diff --git a/README.md b/README.md
index 75964fd..bd1dbd3 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,6 @@ This is a source code of 4in1.ws web site.
- posts and pages are written in Markdown:
- supports syntax highlighting in code blocks
- supports embedding of uploaded files and image resizing
- - tags
- rss feed
- dark theme
- ultra fast on backend:
diff --git a/engine/mysql.php b/engine/mysql.php
index bab8048..9d3a54c 100644
--- a/engine/mysql.php
+++ b/engine/mysql.php
@@ -49,6 +49,8 @@ class mysql {
$count = 0;
foreach ($fields as $k => $v) {
$names[] = $k;
+ if (is_bool($v))
+ $v = (int)$v;
$values[] = $v;
$count++;
}
@@ -118,9 +120,14 @@ class mysql {
function query(string $sql, ...$args): mysqli_result|bool {
$sql = $this->prepareQuery($sql, ...$args);
- $q = $this->link->query($sql);
- if (!$q)
- logError(__METHOD__.': '.$this->link->error."\n$sql\n".backtrace_as_string(1));
+ $q = false;
+ try {
+ $q = $this->link->query($sql);
+ if (!$q)
+ logError(__METHOD__.': '.$this->link->error."\n$sql\n".backtrace_as_string(1));
+ } catch (mysqli_sql_exception $e) {
+ logError(__METHOD__.': '.$e->getMessage()."\n$sql\n".backtrace_as_string(1));
+ }
return $q;
}
diff --git a/engine/request.php b/engine/request.php
index e67d4fb..2992fbf 100644
--- a/engine/request.php
+++ b/engine/request.php
@@ -42,6 +42,7 @@ enum HTTPCode: int {
case MovedPermanently = 301;
case Found = 302;
+ case InvalidRequest = 400;
case Unauthorized = 401;
case NotFound = 404;
case Forbidden = 403;
@@ -51,22 +52,33 @@ enum HTTPCode: int {
}
function http_error(HTTPCode $http_code, string $message = ''): void {
- $ctx = new SkinContext('\\skin\\error');
- $http_message = preg_replace('/(?name);
- $html = $ctx->http_error($http_code->value, $http_message, $message);
- http_response_code($http_code->value);
- echo $html;
- exit;
+ if (is_xhr_request()) {
+ $data = [];
+ if ($message != '')
+ $data['message'] = $message;
+ ajax_error((object)$data, $http_code->value);
+ } else {
+ $ctx = new SkinContext('\\skin\\error');
+ $http_message = preg_replace('/(?name);
+ $html = $ctx->http_error($http_code->value, $http_message, $message);
+ http_response_code($http_code->value);
+ echo $html;
+ exit;
+ }
}
function redirect(string $url, HTTPCode $code = HTTPCode::MovedPermanently): void {
if (!in_array($code, [HTTPCode::MovedPermanently, HTTPCode::Found]))
internal_server_error('invalid http code');
+ if (is_xhr_request()) {
+ ajax_ok(['redirect' => $url]);
+ }
http_response_code($code->value);
header('Location: '.$url);
exit;
}
+function invalid_request(string $message = '') { http_error(HTTPCode::InvalidRequest, $message); }
function internal_server_error(string $message = '') { http_error(HTTPCode::InternalServerError, $message); }
function not_found(string $message = '') { http_error(HTTPCode::NotFound, $message); }
function forbidden(string $message = '') { http_error(HTTPCode::Forbidden, $message); }
@@ -83,6 +95,10 @@ function ajax_response(mixed $data, int $code = 200): void {
exit;
}
+function ensure_admin() {
+ if (!is_admin())
+ forbidden();
+}
abstract class request_handler {
function __construct() {
@@ -90,7 +106,6 @@ abstract class request_handler {
'css/common.css',
'js/common.js'
);
- add_skin_strings_re('/^theme_/');
}
function before_dispatch(string $http_method, string $action) {}
@@ -124,8 +139,12 @@ enum InputVarType: string {
case ENUM = 'e';
}
-function input(string $input): array {
+function input(string $input, array $options = []): array {
global $RouterInput;
+
+ $options = array_merge(['trim' => false], $options);
+ $strval = fn(mixed $val): string => $options['trim'] ? trim((string)$val) : (string)$val;
+
$input = preg_split('/,\s+?/', $input, -1, PREG_SPLIT_NO_EMPTY);
$ret = [];
foreach ($input as $var) {
@@ -133,7 +152,7 @@ function input(string $input): array {
$enum_default = null;
$pos = strpos($var, ':');
- if ($pos !== false) {
+ if ($pos === 1) { // only one-character type specifiers are supported
$type = substr($var, 0, $pos);
$rest = substr($var, $pos + 1);
@@ -177,14 +196,14 @@ function input(string $input): array {
$val = $_GET[$name];
}
if (is_array($val))
- $val = implode($val);
+ $val = $strval(implode($val));
$ret[] = match($vartype) {
InputVarType::INTEGER => (int)$val,
InputVarType::FLOAT => (float)$val,
InputVarType::BOOLEAN => (bool)$val,
- InputVarType::ENUM => !in_array($val, $enum_values) ? $enum_default ?? '' : (string)$val,
- default => (string)$val
+ InputVarType::ENUM => !in_array($val, $enum_values) ? $enum_default ?? '' : $strval($val),
+ default => $strval($val)
};
}
return $ret;
diff --git a/engine/skin.php b/engine/skin.php
index f11eada..45f0bd0 100644
--- a/engine/skin.php
+++ b/engine/skin.php
@@ -12,6 +12,9 @@ $SkinState = new class {
'dynlogo_enabled' => true,
'logo_path_map' => [],
'logo_link_map' => [],
+ 'is_index' => false,
+ 'head_section' => null,
+ 'articles_lang' => null,
];
public array $static = [];
};
@@ -38,10 +41,14 @@ function render($f, ...$vars): void {
$lang[$key] = lang($key);
$lang = !empty($lang) ? json_encode($lang, JSON_UNESCAPED_UNICODE) : '';
+ $title = $SkinState->title;
+ if (!$SkinState->options['is_index'])
+ $title = lang('4in1').' - '.$title;
+
$html = $layout_ctx->layout(
static: $SkinState->static,
theme: $theme,
- title: $SkinState->title,
+ title: $title,
opts: $SkinState->options,
js: $js,
meta: $SkinState->meta,
@@ -182,6 +189,27 @@ class SkinContext {
return csrf_get($key);
}
+ function bc(array $items, ?string $style = null): string {
+ $buf = implode(array_map(function(array $i): string {
+ $buf = '';
+ $has_url = array_key_exists('url', $i);
+
+ if ($has_url)
+ $buf .= '';
+ else
+ $buf .= '';
+ $buf .= htmlescape($i['text']);
+
+ if ($has_url)
+ $buf .= ' ›';
+ else
+ $buf .= '';
+
+ return $buf;
+ }, $items));
+ return '
'.$buf.'
';
+ }
+
protected function _if_condition($condition, $callback, ...$args) {
if (is_string($condition) || $condition instanceof Stringable)
$condition = (string)$condition !== '';
@@ -234,7 +262,7 @@ class SkinString implements Stringable {
return match ($this->modType) {
SkinStringModificationType::HTML => htmlescape($this->string),
SkinStringModificationType::URL => urlencode($this->string),
- SkinStringModificationType::JSON => json_encode($this->string, JSON_UNESCAPED_UNICODE),
+ SkinStringModificationType::JSON => jsonEncode($this->string),
SkinStringModificationType::ADDSLASHES => addslashes($this->string),
default => $this->string,
};
diff --git a/functions.php b/functions.php
index 84d3e4b..c139110 100644
--- a/functions.php
+++ b/functions.php
@@ -23,7 +23,7 @@ function verify_hostname(?string $host = null): void {
}
}
- if (is_cli() && str_ends_with(dirname(__DIR__), 'www-dev'))
+ if (is_cli() && str_ends_with(__DIR__, 'www-dev'))
$config['is_dev'] = true;
}
diff --git a/handler/AdminHandler.php b/handler/AdminHandler.php
index 3e2e080..227631b 100644
--- a/handler/AdminHandler.php
+++ b/handler/AdminHandler.php
@@ -5,6 +5,7 @@ class AdminHandler extends request_handler {
function __construct() {
parent::__construct();
add_static('css/admin.css', 'js/admin.js');
+ add_skin_strings(['error']);
}
function before_dispatch(string $http_method, string $action) {
@@ -13,8 +14,10 @@ class AdminHandler extends request_handler {
}
function GET_index() {
+ $admin_info = admin_current_info();
set_title('$admin_title');
- render('admin/index');
+ render('admin/index',
+ admin_login: $admin_info['login']);
}
function GET_login() {
@@ -26,19 +29,15 @@ class AdminHandler extends request_handler {
function POST_login() {
csrf_check('adminlogin');
- $password = $_POST['password'] ?? '';
- $valid = admin_check_password($password);
- if ($valid) {
- admin_log_auth();
- admin_set_cookie();
- redirect('/admin/');
- }
- forbidden();
+ list($login, $password) = input('login, password');
+ admin_auth($login, $password)
+ ? redirect('/admin/')
+ : forbidden();
}
function GET_logout() {
csrf_check('logout');
- admin_unset_cookie();
+ admin_logout();
redirect('/admin/login/', HTTPCode::Found);
}
@@ -157,26 +156,15 @@ class AdminHandler extends request_handler {
$error_code = 'no_text';
}
- if ($error_code) {
- return $this->_get_pageAdd(
- name: $name,
- title: $title,
- text: $text,
- error_code: $error_code
- );
- }
+ if ($error_code)
+ ajax_error(['code' => $error_code]);
if (!pages::add([
'short_name' => $name,
'title' => $title,
'md' => $text
])) {
- return $this->_get_pageAdd(
- name: $name,
- title: $title,
- text: $text,
- error_code: 'db_err'
- );
+ ajax_error(['code' => 'db_err']);
}
$page = pages::getByName($name);
@@ -184,97 +172,230 @@ class AdminHandler extends request_handler {
}
function GET_post_add() {
- return $this->_get_postAdd();
+ add_skin_strings_re('/^(err_)?blog_/');
+ set_title('$blog_write');
+ static::make_wide();
+
+ $js_texts = [];
+ foreach (PostLanguage::cases() as $pl) {
+ $js_texts[$pl->value] = [
+ 'title' => '',
+ 'md' => '',
+ 'toc' => false,
+ ];
+ }
+
+ render('admin/postForm',
+ title: '',
+ text: '',
+ langs: PostLanguage::cases(),
+ short_name: '',
+ js_texts: $js_texts,
+ lang: PostLanguage::getDefault()->value);
}
function POST_post_add() {
- csrf_check('addpost');
+ if (!is_xhr_request())
+ invalid_request();
- list($text, $title, $tags, $visible, $short_name)
- = input('text, title, tags, b:visible, short_name');
- $tags = tags::splitString($tags);
+ csrf_check('post_add');
+
+ list($visibility_enabled, $short_name, $langs, $date)
+ = input('b:visible, short_name, langs, date');
+
+ self::_postEditValidateCommonData($date);
+
+ $lang_data = [];
+ $at_least_one_lang_is_written = false;
+ foreach (PostLanguage::cases() as $lang) {
+ list($title, $text, $toc_enabled) = input("title:{$lang->value}, text:{$lang->value}, b:toc:{$lang->value}", ['trim' => true]);
+ if ($title !== '' && $text !== '') {
+ $lang_data[$lang->value] = [$title, $text, $toc_enabled];
+ $at_least_one_lang_is_written = true;
+ }
+ }
$error_code = null;
- if (!$title) {
- $error_code = 'no_title';
- } else if (!$text) {
+ if (!$at_least_one_lang_is_written) {
$error_code = 'no_text';
- } else if (empty($tags)) {
- $error_code = 'no_tags';
} else if (empty($short_name)) {
$error_code = 'no_short_name';
}
-
if ($error_code)
- return $this->_get_postAdd(
- title: $title,
- text: $text,
- tags: $tags,
- short_name: $short_name,
- error_code: $error_code
- );
+ ajax_error(['code' => $error_code]);
- $id = posts::add([
- 'title' => $title,
- 'md' => $text,
- 'visible' => (int)$visible,
+ $post = posts::add([
+ 'visible' => $visibility_enabled,
'short_name' => $short_name,
+ 'date' => $date
]);
- if (!$id)
- $this->_get_postAdd(
- title: $title,
- text: $text,
- tags: $tags,
- short_name: $short_name,
- error_code: 'db_err'
- );
+ if (!$post)
+ ajax_error(['code' => 'db_err', 'message' => 'failed to add post']);
- // set tags
- $post = posts::get($id);
- $tag_ids = array_values(tags::getTags($tags));
- $post->setTagIds($tag_ids);
+ // add texts
+ foreach ($lang_data as $lang => $data) {
+ list($title, $text, $toc_enabled) = $data;
+ if (!$post->addText(
+ lang: PostLanguage::from($lang),
+ title: $title,
+ md: $text,
+ toc: $toc_enabled)
+ ) {
+ posts::delete($post);
+ ajax_error(['code' => 'db_err', 'message' => 'failed to add text language '.$lang]);
+ }
+ }
- redirect($post->getUrl());
+ // done
+ ajax_ok(['url' => $post->getUrl()]);
}
- function GET_auto_delete() {
+ protected static function _postEditValidateCommonData($date) {
+ $dt = DateTime::createFromFormat("Y-m-d", $date);
+ $date_is_valid = $dt && $dt->format("Y-m-d") === $date;
+ if (!$date_is_valid)
+ ajax_error(['code' => 'no_date']);
+ }
+
+ function GET_page_delete() {
+ list($name) = input('short_name');
+
+ $page = pages::getByName($name);
+ if (!$page)
+ not_found();
+
+ csrf_check('delpage'.$page->shortName);
+ pages::delete($page);
+ redirect('/');
+ }
+
+ function GET_post_delete() {
list($name) = input('short_name');
$post = posts::getByName($name);
- if ($post) {
- csrf_check('delpost'.$post->id);
- posts::delete($post);
- redirect('/');
- }
+ if (!$post)
+ not_found();
- $page = pages::getByName($name);
- if ($page) {
- csrf_check('delpage'.$page->shortName);
- pages::delete($page);
- redirect('/');
+ csrf_check('delpost'.$post->id);
+ posts::delete($post);
+ redirect('/articles/');
+ }
+
+ function GET_post_edit() {
+ list($short_name, $saved, $lang) = input('short_name, b:saved, lang');
+ $lang = PostLanguage::from($lang);
+
+ $post = posts::getByName($short_name);
+ if ($post) {
+ $texts = $post->getTexts();
+ if (!isset($texts[$lang->value]))
+ not_found();
+
+ $js_texts = [];
+ foreach (PostLanguage::cases() as $pl) {
+ if (isset($texts[$pl->value])) {
+ $text = $texts[$pl->value];
+ $js_texts[$pl->value] = [
+ 'title' => $text->title,
+ 'md' => $text->md,
+ 'toc' => $text->toc,
+ ];
+ } else {
+ $js_texts[$pl->value] = [
+ 'title' => '',
+ 'md' => '',
+ 'toc' => false,
+ ];
+ }
+ }
+
+ $text = $texts[$lang->value];
+
+ add_skin_strings_re('/^(err_)?blog_/');
+ add_skin_strings(['blog_post_edit_title']);
+ set_title(lang('blog_post_edit_title', $text->title));
+ static::make_wide();
+ render('admin/postForm',
+ is_edit: true,
+ post_id: $post->id,
+ post_url: $post->getUrl(),
+ title: $text->title,
+ text: $text->md,
+ date: $post->getDateForInputField(),
+ visible: $post->visible,
+ toc: $text->toc,
+ saved: $saved,
+ short_name: $short_name,
+ langs: PostLanguage::cases(),
+ lang: $text->lang->value,
+ js_texts: $js_texts
+ );
}
not_found();
}
+ function POST_post_edit() {
+ if (!is_xhr_request())
+ invalid_request();
+
+ list($old_short_name, $short_name, $langs, $date) = input('short_name, new_short_name, langs, date');
+
+ $post = posts::getByName($old_short_name);
+ if (!$post)
+ not_found();
+
+ csrf_check('editpost'.$post->id);
+
+ self::_postEditValidateCommonData($date);
+
+ if (empty($short_name))
+ ajax_error(['code' => 'no_short_name']);
+
+ foreach (explode(',', $langs) as $lang) {
+ $lang = PostLanguage::from($lang);
+ list($text, $title, $visible, $toc) = input("text:{$lang->value}, title:{$lang->value}, b:visible, b:toc:{$lang->value}");
+
+ $error_code = null;
+ if (!$title)
+ $error_code = 'no_title';
+ else if (!$text)
+ $error_code = 'no_text';
+ if ($error_code)
+ ajax_error(['code' => $error_code]);
+
+ $pt = $post->getText($lang);
+ if (!$pt) {
+ $pt = $post->addText(
+ lang: $lang,
+ title: $title,
+ md: $text,
+ toc: $toc
+ );
+ if (!$pt)
+ ajax_error(['code' => 'db_err']);
+ } else {
+ $pt->edit([
+ 'title' => $title,
+ 'md' => $text,
+ 'toc' => (int)$toc
+ ]);
+ }
+ }
+
+ $post_data = ['date' => $date, 'visible' => $visible];
+ if ($post->shortName != $short_name)
+ $post_data['short_name'] = $short_name;
+ $post->edit($post_data);
+
+ ajax_ok(['url' => $post->getUrl().'edit/?saved=1&lang='.$lang->value]);
+
+ }
+
function GET_auto_edit() {
list($short_name, $saved) = input('short_name, b:saved');
- $post = posts::getByName($short_name);
- if ($post) {
- $tags = $post->getTags();
- return $this->_get_postEdit($post,
- title: $post->title,
- text: $post->md,
- tags: $post->getTags(),
- visible: $post->visible,
- toc: $post->toc,
- short_name: $post->shortName,
- saved: $saved,
- );
- }
-
$page = pages::getByName($short_name);
if ($page) {
return $this->_get_pageEdit($page,
@@ -291,50 +412,6 @@ class AdminHandler extends request_handler {
function POST_auto_edit() {
list($short_name) = input('short_name');
- $post = posts::getByName($short_name);
- if ($post) {
- csrf_check('editpost'.$post->id);
-
- list($text, $title, $tags, $visible, $toc, $short_name)
- = input('text, title, tags, b:visible, b:toc, new_short_name');
-
- $tags = tags::splitString($tags);
- $error_code = null;
-
- if (!$title) {
- $error_code = 'no_title';
- } else if (!$text) {
- $error_code = 'no_text';
- } else if (empty($tags)) {
- $error_code = 'no_tags';
- } else if (empty($short_name)) {
- $error_code = 'no_short_name';
- }
-
- if ($error_code)
- $this->_get_postEdit($post,
- title: $title,
- text: $text,
- tags: $tags,
- visible: $visible,
- toc: $toc,
- short_name: $short_name,
- error_code: $error_code
- );
-
- $post->edit([
- 'title' => $title,
- 'md' => $text,
- 'visible' => (int)$visible,
- 'toc' => (int)$toc,
- 'short_name' => $short_name
- ]);
- $tag_ids = array_values(tags::getTags($tags));
- $post->setTagIds($tag_ids);
-
- redirect($post->getUrl().'edit/?saved=1');
- }
-
$page = pages::getByName($short_name);
if ($page) {
csrf_check('editpage'.$page->shortName);
@@ -359,7 +436,6 @@ class AdminHandler extends request_handler {
title: $title,
text: $text,
visible: $visible,
- error_code: $error_code
);
}
@@ -376,7 +452,7 @@ class AdminHandler extends request_handler {
not_found();
}
- protected static function setWidePage() {
+ protected static function make_wide() {
set_skin_opts([
'full_width' => true,
'no_footer' => true
@@ -386,17 +462,15 @@ class AdminHandler extends request_handler {
protected function _get_pageAdd(
string $name,
string $title = '',
- string $text = '',
- ?string $error_code = null
+ string $text = ''
) {
add_skin_strings_re('/^(err_)?pages_/');
set_title(lang('pages_create_title', $name));
- static::setWidePage();
+ static::make_wide();
render('admin/pageForm',
short_name: $name,
title: $title,
- text: $text,
- error_code: $error_code);
+ text: $text);
}
protected function _get_pageEdit(
@@ -409,62 +483,14 @@ class AdminHandler extends request_handler {
) {
add_skin_strings_re('/^(err_)?pages_/');
set_title(lang('pages_page_edit_title', $page->shortName.'.html'));
- static::setWidePage();
+ static::make_wide();
render('admin/pageForm',
is_edit: true,
short_name: $page->shortName,
title: $title,
text: $text,
visible: $visible,
- saved: $saved,
- error_code: $error_code);
- }
-
- protected function _get_postEdit(
- Post $post,
- string $title = '',
- string $text = '',
- ?array $tags = null,
- bool $visible = false,
- bool $toc = false,
- string $short_name = '',
- ?string $error_code = null,
- bool $saved = false,
- ) {
- add_skin_strings_re('/^(err_)?blog_/');
- set_title(lang('blog_post_edit_title', $post->title));
- static::setWidePage();
- render('admin/postForm',
- is_edit: true,
- post_id: $post->id,
- post_url: $post->getUrl(),
- title: $title,
- text: $text,
- tags: $tags ? implode(', ', $tags) : '',
- visible: $visible,
- toc: $toc,
- saved: $saved,
- short_name: $short_name,
- error_code: $error_code
- );
- }
-
- protected function _get_postAdd(
- string $title = '',
- string $text = '',
- ?array $tags = null,
- string $short_name = '',
- ?string $error_code = null
- ) {
- add_skin_strings_re('/^(err_)?blog_/');
- set_title('$blog_write');
- static::setWidePage();
- render('admin/postForm',
- title: $title,
- text: $text,
- tags: $tags ? implode(', ', $tags) : '',
- short_name: $short_name,
- error_code: $error_code);
+ saved: $saved);
}
}
\ No newline at end of file
diff --git a/handler/MainHandler.php b/handler/MainHandler.php
index 7994ec5..14199bc 100644
--- a/handler/MainHandler.php
+++ b/handler/MainHandler.php
@@ -4,29 +4,16 @@ class MainHandler extends request_handler {
function GET_index() {
set_title('$site_title');
+ set_skin_opts(['is_index' => true]);
render('main/index');
}
function GET_about() { redirect('/info/'); }
function GET_contacts() { redirect('/info/'); }
- function GET_auto() {
+ function GET_page() {
list($name) = input('name');
- if (is_admin()) {
- if (is_numeric($name)) {
- $post = posts::get((int)$name);
- } else {
- $post = posts::getByName($name);
- }
- if ($post)
- return $this->renderPost($post);
-
- $tag = tags::get($name);
- if ($tag)
- return $this->renderTag($tag);
- }
-
$page = pages::getByName($name);
if ($page)
return $this->renderPage($page);
@@ -40,58 +27,77 @@ class MainHandler extends request_handler {
not_found();
}
- protected function renderPost(Post $post) {
+ function GET_post() {
global $config;
- if (!$post->visible && !is_admin())
- not_found();
+ ensure_admin();
- $tags = $post->getTags();
+ list($name, $input_lang) = input('name, lang');
- add_meta(
- ['property' => 'og:title', 'content' => $post->title],
- ['property' => 'og:url', 'content' => $config['domain'].$post->getUrl()]
- );
- if (($img = $post->getFirstImage()) !== null)
- add_meta(['property' => 'og:image', 'content' => $img->getDirectUrl()]);
+ $lang = null;
+ try {
+ if ($input_lang)
+ $lang = PostLanguage::from($input_lang);
+ } catch (ValueError $e) {
+ not_found($e->getMessage());
+ }
- add_meta([
- 'name' => 'description',
- 'property' => 'og:description',
- 'content' => $post->getDescriptionPreview(155)
- ]);
+ if (!$lang)
+ $lang = PostLanguage::getDefault();
- set_title($post->title);
+ $post = posts::getByName($name);
- if ($post->toc)
- set_skin_opts(['wide' => true]);
+ if ($post) {
+ if ($lang == PostLanguage::getDefault() && $input_lang == $lang->value)
+ redirect($post->getUrl());
+ if (!$post->hasLang($lang))
+ not_found('no text for language '.$lang->name);
+ if (!$post->visible && !is_admin())
+ not_found();
- render('main/post',
- title: $post->title,
- id: $post->id,
- unsafe_html: $post->getHtml(is_retina(), getUserTheme()),
- unsafe_toc_html: $post->getToc(),
- date: $post->getFullDate(),
- tags: $tags,
- visible: $post->visible,
- url: $post->getUrl(),
- email: $config['admin_email'],
- urlencoded_reply_subject: 'Re: '.$post->title);
- }
+ $pt = $post->getText($lang);
- protected function renderTag(Tag $tag) {
- $tag = tags::get($tag);
- if (!is_admin() && !$tag->visiblePostsCount)
- not_found();
+ $other_langs = [];
+ foreach (PostLanguage::cases() as $pl) {
+ if ($pl == $lang)
+ continue;
+ if ($post->hasLang($pl))
+ $other_langs[] = $pl->value;
+ }
- $count = posts::getCountByTagId($tag->id, is_admin());
- $posts = $count ? posts::getPostsByTagId($tag->id, is_admin()) : [];
+ add_meta(
+ ['property' => 'og:title', 'content' => $pt->title],
+ ['property' => 'og:url', 'content' => $config['domain'].$post->getUrl()]
+ );
+ if (($img = $pt->getFirstImage()) !== null)
+ add_meta(['property' => 'og:image', 'content' => $img->getDirectUrl()]);
- set_title('#'.$tag->tag);
- render('main/tag',
- count: $count,
- posts: $posts,
- tag: $tag->tag);
+ add_meta([
+ 'name' => 'description',
+ 'property' => 'og:description',
+ 'content' => $pt->getDescriptionPreview(155)
+ ]);
+
+ set_skin_opts(['articles_lang' => $lang->value]);
+
+ set_title($pt->title);
+
+ if ($pt->hasTableOfContents())
+ set_skin_opts(['wide' => true]);
+
+ render('main/post',
+ title: $pt->title,
+ id: $post->id,
+ unsafe_html: $pt->getHtml(is_retina(), getUserTheme()),
+ unsafe_toc_html: $pt->getTableOfContentsHtml(),
+ date: $post->getFullDate(),
+ visible: $post->visible,
+ url: $post->getUrl(),
+ lang: $lang->value,
+ other_langs: $other_langs);
+ }
+
+ not_found();
}
protected function renderPage(Page $page) {
@@ -100,6 +106,9 @@ class MainHandler extends request_handler {
if (!is_admin() && !$page->visible && $page->get_id() != $config['index_page_id'])
not_found();
+ if ($page->shortName == 'info')
+ set_skin_opts(['head_section' => 'about']);
+
set_title($page ? $page->title : '???');
render('main/page',
unsafe_html: $page->getHtml(is_retina(), getUserTheme()),
@@ -110,12 +119,16 @@ class MainHandler extends request_handler {
function GET_rss() {
global $config;
- $items = array_map(fn(Post $post) => [
- 'title' => $post->title,
- 'link' => $post->getUrl(),
- 'pub_date' => date(DATE_RSS, $post->ts),
- 'description' => $post->getDescriptionPreview(500),
- ], posts::getList(0, 20));
+ $lang = PostLanguage::getDefault();
+ $items = array_map(function(Post $post) use ($lang) {
+ $pt = $post->getText($lang);
+ return [
+ 'title' => $pt->title,
+ 'link' => $post->getUrl(),
+ 'pub_date' => date(DATE_RSS, $post->ts),
+ 'description' => $pt->getDescriptionPreview(500)
+ ];
+ }, posts::getList(0, 20, filter_by_lang: $lagn));
$ctx = new SkinContext('\\skin\\rss');
$body = $ctx->atom(
@@ -130,9 +143,27 @@ class MainHandler extends request_handler {
}
function GET_articles() {
- $posts = posts::getList(0, 1000);
+ ensure_admin();
+
+ list($lang) = input('lang');
+ if ($lang) {
+ $lang = PostLanguage::tryFrom($lang);
+ if (!$lang || $lang == PostLanguage::getDefault())
+ redirect('/articles/');
+ } else {
+ $lang = PostLanguage::getDefault();
+ }
+
+ $posts = posts::getList(
+ include_hidden: is_admin(),
+ filter_by_lang: $lang);
+
set_title('$articles');
- render('main/articles', posts: $posts);
+ set_skin_opts(['head_section' => 'articles']);
+
+ render('main/articles',
+ posts: $posts,
+ selected_lang: $lang);
}
}
\ No newline at end of file
diff --git a/helper/ArticlesHelper.php b/helper/ArticlesHelper.php
new file mode 100644
index 0000000..bfe8f69
--- /dev/null
+++ b/helper/ArticlesHelper.php
@@ -0,0 +1,19 @@
+ {
+ this.tocByLang[this.getCurrentLang()] = e.target.checked
+ })
+ }
+
+ let lang = 'en'
+ if (this.isPost()) {
+ lang = this.getCurrentLang()
+ this.form.lang.addEventListener('change', (e) => {
+ let newLang = e.target.options[e.target.selectedIndex].value
+ this.draft.setLang(newLang)
+ this.fillFromDraft({applyEvenEmpty: true})
+ ge('toc_cb').checked = this.tocByLang[newLang]
+ })
+ }
+
+ let draftId, draftType = !this.isPost() ? 'page' : 'post'
+ if (this.isEditing()) {
+ draftId = `edit_${draftType}${this.opts.id}`
+ } else {
+ draftId = `new_${draftType}`
+ }
+
+ this.draft = new Draft(draftId, lang);
+ if (this.isEditing()) {
+ for (let l in opts.texts) {
+ this.draft.setLang(l)
+ this.draft.title = opts.texts[l].title
+ this.draft.text = opts.texts[l].md
+ this.tocByLang[l] = opts.texts[l].toc
+ }
+ this.draft.setLang(lang)
+ this.showPreview()
+ } else {
+ this.fillFromDraft()
+ }
+
+ // window.addEventListener('scroll', this.onScroll)
+ // window.addEventListener('resize', this.onResize)
+ }
+
+ getCurrentLang() { return this.form.lang.options[this.form.lang.selectedIndex].value }
+ isPost() { return !this.opts.pages }
+ isPage() { return !!this.opts.pages }
+ isEditing() { return !!this.opts.edit }
+
+ fillFromDraft({applyEvenEmpty} = {applyEvenEmpty: false}) {
+ for (const what of ['title', 'text']) {
+ if (this.draft[what] !== '' || applyEvenEmpty)
+ this.form[what].value = this.draft[what]
+ }
+
+ if (this.form.text.value !== '' || applyEvenEmpty)
+ this.showPreview()
+ }
+
+ showPreview() {
+ if (this.previewRequest !== null)
+ this.previewRequest.abort()
+ const params = {
+ md: this.form.elements.text.value,
+ use_image_previews: this.isPage() ? 1 : 0
+ }
+ if (this.isPost())
+ params.title = this.form.elements.title.value
+ this.previewRequest = ajax.post('/admin/markdown-preview.ajax', params, (err, response) => {
+ if (err)
+ return console.error(err)
+ ge('preview_html').innerHTML = response.html
+ })
+ }
+
+ showError(code, message) {
+ if (code) {
+ let el = ge('form-error')
+ let label = escape(lang(`err_blog_${code}`))
+ if (message)
+ label += ' (' + message + ')'
+ el.innerHTML = label
+ el.style.display = 'block'
+ } else if (message) {
+ alert(lang('error')+': '+message)
+ }
+ }
+
+ hideError() {
+ let el = ge('form-error')
+ el.style.display = 'none'
+ }
+
+ onSubmit = (evt) => {
+ const fields = []
try {
- var fields = ['title', 'text'];
- if (!this.opts.pages)
- fields.push('tags');
- if (this.opts.edit) {
+ if (this.isEditing()) {
fields.push('new_short_name');
} else {
fields.push('short_name');
}
- for (var i = 0; i < fields.length; i++) {
- var field = fields[i];
- if (event.target.elements[field].value.trim() === '')
+ for (const field of fields) {
+ if (evt.target.elements[field].value.trim() === '')
throw 'no_'+field
}
- // Draft.reset();
+ const fd = new FormData()
+ for (const f of fields) {
+ console.log(`field: ${f}`)
+ fd.append(f, evt.target[f].value.trim())
+ }
+
+ // fd.append('lang', this.getCurrentLang())
+ fd.append('visible', ge('visible_cb').checked ? 1 : 0)
+
+ // language-specific fields
+ let atLeastOneLangIsWritten = false
+ const writtenLangs = []
+ for (const l of this.opts.langs) {
+ let title = this.draft.getForLang(l, 'title')
+ let text = this.draft.getForLang(l, 'text')
+ console.log(`lang: ${l}`, title, text)
+ if (title !== '' && text !== '')
+ atLeastOneLangIsWritten = true
+ fd.append(`title:${l}`, title)
+ fd.append(`text:${l}`, text)
+ fd.append(`toc:${l}`, this.tocByLang[l] ? 1 : 0)
+ writtenLangs.push(l)
+ }
+ if (!atLeastOneLangIsWritten)
+ throw 'no_text'
+
+ fd.append('langs', writtenLangs.join(','))
+
+ // date field
+ const dateInput = evt.target.elements.date;
+ if (!dateInput.value)
+ throw 'no_date'
+ fd.append('date', dateInput.value)
+
+ fd.append('token', this.opts.token)
+ cancelEvent(evt)
+
+ this.hideError();
+
+ ajax.post(evt.target.action, fd, (error, response) => {
+ if (error) {
+ this.showError(error.code, error.message)
+ return;
+ }
+
+ if (response.url) {
+ this.draft.reset(this.opts.langs);
+ window.location = response.url
+ }
+ })
} catch (e) {
- var error = typeof e == 'string' ? lang((this.opts.pages ? 'err_pages_' : 'err_blog_')+e) : e.message;
- alert(error);
+ const errorText = typeof e == 'string' ? lang('error')+': '+lang((this.isPage() ? 'err_pages_' : 'err_blog_')+e) : e.message;
+ alert(errorText);
console.error(e);
- return cancelEvent(event);
+ return cancelEvent(evt);
}
- },
+ }
- onToggleWrapClick: function(e) {
- var textarea = this.form.elements.text;
+ onToggleWrapClick = (e) => {
+ const textarea = this.form.elements.text
if (!hasClass(textarea, 'nowrap')) {
- addClass(textarea, 'nowrap');
+ addClass(textarea, 'nowrap')
} else {
- removeClass(textarea, 'nowrap');
+ removeClass(textarea, 'nowrap')
}
- return cancelEvent(e);
- },
+ return cancelEvent(e)
+ }
- onInput: function(e) {
- if (this.previewTimeout !== null) {
+ onInput = (e) => {
+ if (this.previewTimeout !== null)
clearTimeout(this.previewTimeout);
- }
- this.previewTimeout = setTimeout(function() {
+
+ this.previewTimeout = setTimeout(() => {
this.previewTimeout = null;
this.showPreview();
- // Draft[e.target.name === 'title' ? 'setTitle' : 'setText'](e.target.value);
- }.bind(this), 300);
- },
+ const what = e.target.name === 'title' ? 'title' : 'text'
+ this.draft[what] = e.target.value
+ }, 300)
+ }
- onScroll: function() {
+ onScroll = () => {
var ANCHOR_TOP = 10;
var y = window.pageYOffset;
@@ -124,21 +234,22 @@ var AdminWriteForm = {
this.isFixed = false;
}
- },
-
- onResize: function() {
- if (this.isFixed) {
- var form = this.form;
- var td = ge('form_first_cell');
- var ph = ge('form_placeholder');
-
- var rect = td.getBoundingClientRect();
- var pr = parseInt(getComputedStyle(td).paddingRight, 10) || 0;
-
- ph.style.height = form.getBoundingClientRect().height+'px';
- form.style.width = (rect.width - pr) + 'px';
- }
}
-};
-bindEventHandlers(AdminWriteForm);
+ onResize = () => {
+ if (!this.isFixed)
+ return
+
+ const form = this.form
+ const td = ge('form_first_cell')
+ const ph = ge('form_placeholder')
+
+ const rect = td.getBoundingClientRect()
+ const pr = parseInt(getComputedStyle(td).paddingRight, 10) || 0
+
+ ph.style.height = form.getBoundingClientRect().height+'px'
+ form.style.width = (rect.width - pr) + 'px'
+ }
+}
+
+// bindEventHandlers(AdminWriteForm);
diff --git a/htdocs/js/common/02-ajax.js b/htdocs/js/common/02-ajax.js
index 59e867b..0e5d356 100644
--- a/htdocs/js/common/02-ajax.js
+++ b/htdocs/js/common/02-ajax.js
@@ -8,9 +8,8 @@
};
function createXMLHttpRequest() {
- if (window.XMLHttpRequest) {
+ if (window.XMLHttpRequest)
return new XMLHttpRequest();
- }
var xhr;
try {
@@ -59,7 +58,7 @@
break;
case 'POST':
- if (isObject(data)) {
+ if (isObject(data) && !(data instanceof FormData)) {
var sdata = [];
for (var k in data) {
if (data.hasOwnProperty(k)) {
@@ -77,32 +76,33 @@
xhr.open(method, url);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
- if (method === 'POST') {
- xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
- }
+ if (method === 'POST' && !(data instanceof FormData))
+ xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
+ var callbackFired = false;
xhr.onreadystatechange = function() {
+ if (callbackFired)
+ return
+
if (xhr.readyState === 4) {
- if ('status' in xhr && !/^2|1223/.test(xhr.status)) {
- throw new Error('http code '+xhr.status)
- }
if (opts.json) {
var resp = JSON.parse(xhr.responseText)
- if (!isObject(resp)) {
+ if (!isObject(resp))
throw new Error('ajax: object expected')
- }
- if (resp.error) {
- throw new Error(resp.error)
- }
- callback(null, resp.response);
+
+ callbackFired = true;
+ if (resp.error)
+ callback(resp.error, null, xhr.status);
+ else
+ callback(null, resp.response, xhr.status);
} else {
- callback(null, xhr.responseText);
+ callback(null, xhr.responseText, xhr.status);
}
}
};
xhr.onerror = function(e) {
- callback(e);
+ callback(e, null, 0);
};
xhr.send(method === 'GET' ? null : data);
diff --git a/htdocs/scss/app/blog.scss b/htdocs/scss/app/blog.scss
index c0e3ca8..23874d9 100644
--- a/htdocs/scss/app/blog.scss
+++ b/htdocs/scss/app/blog.scss
@@ -45,14 +45,15 @@
padding-top: 12px;
}
td:nth-child(1) {
- width: 70%;
+ width: 40%;
}
- td:nth-child(2) {
+ td:nth-child(2),
+ td:nth-child(3) {
width: 30%;
padding-left: 10px;
}
tr:first-child td {
- padding-top: 0px;
+ padding-top: 0;
}
button[type="submit"] {
margin-left: 3px;
@@ -177,27 +178,9 @@ body.wide .blog-post {
color: $grey;
margin-top: 5px;
font-size: $fs - 1px;
- > a {
- margin-left: 5px;
- }
-}
-
-.blog-post-tags {
- margin-top: 16px;
- margin-bottom: -1px;
-}
-.blog-post-tags > a {
- display: block;
- float: left;
- font-size: $fs - 1px;
- margin-right: 8px;
- cursor: pointer;
-}
-.blog-post-tags > a:last-child {
- margin-right: 0;
-}
-.blog-post-tags > a > span {
- opacity: 0.5;
+ //> a {
+ // margin-left: 5px;
+ //}
}
.blog-post-text {}
@@ -206,14 +189,14 @@ body.wide .blog-post {
margin: 13px 0;
}
- p {
- margin-top: 13px;
- margin-bottom: 13px;
+ p, center {
+ margin-top: 20px;
+ margin-bottom: 20px;
}
- p:first-child {
+ p:first-child, center:first-child {
margin-top: 0;
}
- p:last-child {
+ p:last-child, center:last-child {
margin-bottom: 0;
}
@@ -246,10 +229,11 @@ body.wide .blog-post {
}
blockquote {
- border-left: 3px $border-color solid;
+ border-left: 2px $quote_line solid;
margin-left: 0;
padding: 5px 0 5px 12px;
- color: $grey;
+ color: $quote_color;
+ font-style: italic;
&:first-child {
padding-top: 0;
margin-top: 0;
@@ -367,27 +351,22 @@ body.wide .blog-post {
margin-left: 2px;
}
-$blog-tags-width: 175px;
-
.index-blog-block {
margin-top: 23px;
}
.blog-list {}
-.blog-list.withtags {
- margin-right: $blog-tags-width + $base-padding*2;
-}
-.blog-list-title {
- font-size: 22px;
- margin-bottom: 15px;
- > span {
+.blog-item-right-links {
+ font-size: 16px;
+ float: right;
+ > a {
margin-left: 2px;
- > a {
- font-size: 16px;
- margin-left: 2px;
- }
}
}
+.blog-links-separator {
+ color: $grey;
+}
+
.blog-list-table-wrap {
padding: 5px 0;
}
@@ -436,41 +415,3 @@ td.blog-item-title-cell {
}
}
-/*
-a.blog-item-view-all-link {
- display: inline-block;
- padding: 4px 17px;
- @include radius(5px);
- background-color: #f4f4f4;
- color: #555;
- margin-top: 2px;
-}
-a.blog-item-view-all-link:hover {
- text-decoration: none;
- background-color: #ededed;
-}
-*/
-
-.blog-tags {
- float: right;
- width: $blog-tags-width;
- padding-left: $base-padding - 10px;
- border-left: 1px $border-color solid;
-}
-.blog-tags-title {
- margin-bottom: 15px;
- font-size: 22px;
- padding: 0 7px;
-}
-.blog-tag-item {
- padding: 6px 10px;
- font-size: $fs - 1px;
-}
-.blog-tag-item > a {
- color: $fg;
-}
-.blog-tag-item-count {
- color: #aaa;
- margin-left: 6px;
- text-decoration: none !important;
-}
diff --git a/htdocs/scss/app/common.scss b/htdocs/scss/app/common.scss
index 4243ad9..3754c36 100644
--- a/htdocs/scss/app/common.scss
+++ b/htdocs/scss/app/common.scss
@@ -51,7 +51,6 @@ textarea {
appearance: none;
box-sizing: border-box;
border: 1px $input-border solid;
- border-radius: 0;
background-color: $input-bg;
color: $fg;
font-family: $ff;
@@ -64,34 +63,30 @@ textarea {
border-color: $input-border-focused;
}
}
-
textarea {
resize: vertical;
}
-//input[type="checkbox"] {
-// margin-left: 0;
-//}
+button {
+ @include radius(3px);
+ background-color: $light-bg;
+ color: $fg;
+ padding: 6px 12px;
+ border: 1px $input-border solid;
+ font-family: $ff;
+ font-size: $fs;
+ outline: none;
+ cursor: pointer;
+ position: relative;
+}
+button:hover {
+ background-color: $input-border;
+}
+button:active {
+ border-color: $input-border-focused;
+ background-color: $input-border-focused;
+}
-//button {
-// border-radius: 2px;
-// background-color: $light-bg;
-// color: $fg;
-// padding: 7px 12px;
-// border: none;
-// /*box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);*/
-// font-family: $ff;
-// font-size: $fs - 1px;
-// outline: none;
-// cursor: pointer;
-// position: relative;
-//}
-//button:hover {
-// box-shadow: 0 1px 9px rgba(0, 0, 0, 0.2);
-//}
-//button:active {
-// top: 1px;
-//}
a {
text-decoration: none;
@@ -360,4 +355,38 @@ a.index-dl-line {
background-color: $link-color-underline;
}
}
+}
+
+.bc {
+ padding-bottom: 15px;
+
+ > a.bc-item, > span.bc-item {
+ font-size: $fs + 2px;
+ display: inline-block;
+ padding: 5px 10px;
+
+ &:not(:first-child) {
+ margin-left: 10px;
+ }
+ }
+
+ > a.bc-item {
+ background-color: $hover-hl;
+ border-radius: 4px;
+ }
+ > span.bc-item {
+ padding: 5px;
+ font-weight: 600;
+ }
+
+ > a.bc-item:hover {
+ text-decoration: none;
+ border-bottom-color: $link-color-underline;
+ background-color: $hover-hl-darker;
+ }
+
+ // arrow
+ span.bc-arrow {
+ color: $grey !important;
+ }
}
\ No newline at end of file
diff --git a/htdocs/scss/app/head.scss b/htdocs/scss/app/head.scss
index 7ee8b56..15e8e6f 100644
--- a/htdocs/scss/app/head.scss
+++ b/htdocs/scss/app/head.scss
@@ -2,7 +2,6 @@
display: table;
width: 100%;
border-collapse: collapse;
- //border-bottom: 1px $border-color solid;
}
.head-inner {
display: table-row;
@@ -22,23 +21,16 @@
background-color: transparent;
display: inline-block;
- &:hover {
- border-radius: 4px;
- background-color: $hover-hl;
- }
-
> a:hover {
text-decoration: none;
}
&-title {
+ color: $fg;
padding-bottom: 3px;
&-author {
font-weight: normal;
color: $grey;
- //font-size: $fs;
- //position: relative;
- //top: -5px;
}
}
@@ -49,7 +41,36 @@
padding-top: 2px;
line-height: 18px;
}
+
}
+.head.no-subtitle .head-logo {
+ padding-bottom: 0;
+
+ > a {
+ border-bottom: 1px transparent solid;
+ display: inline-block;
+
+ &:hover {
+ border-bottom: 1px $border-color solid;
+ }
+
+ .head-logo-subtitle,
+ .head-logo-title {
+ display: block;
+ float: left;
+ }
+
+ .head-logo-subtitle {
+ margin-left: 10px;
+ font-size: $fs;
+ }
+
+ .head-logo-subtitle > br {
+ display: none;
+ }
+ }
+}
+
//body:not(.theme-changing) .head-logo {
// @include transition(background-color, 0.03s);
//}
@@ -57,8 +78,9 @@
.head-items {
text-align: right;
display: table-cell;
- vertical-align: middle;
+ vertical-align: top;
color: $dark-grey; // color of separators
+ padding-top: 15px;
}
a.head-item {
color: $fg;
@@ -79,7 +101,7 @@ a.head-item {
}
}
- &:hover {
+ &:hover, &.is-selected {
border-radius: 4px;
background-color: $hover-hl;
text-decoration: none;
diff --git a/htdocs/scss/app/mobile.scss b/htdocs/scss/app/mobile.scss
index 883c857..a9cfb4e 100644
--- a/htdocs/scss/app/mobile.scss
+++ b/htdocs/scss/app/mobile.scss
@@ -4,18 +4,26 @@ textarea {
-webkit-overflow-scrolling: touch;
}
-.head, .head-inner, .head-logo-wrap, .head-items, head-logo {
+.page-content {
+ padding: 0 15px;
+}
+
+.head, .head-inner, .head-logo-wrap, .head-items, .head-logo {
display: block;
}
+.head {
+ overflow: hidden;
+}
+.head-logo-wrap {
+ margin-left: -20px;
+ margin-right: -20px;
+ border-bottom: 1px $border-color solid;
+
+ padding-top: 5px;
+ padding-bottom: 5px;
+}
.head-logo {
margin: 0;
-}
-.head-logo-wrap {
- padding-bottom: 4px;
-}
-.head-logo {
- border: 1px $border-color solid;
- border-radius: 6px;
display: block;
text-align: center;
font-size: $fs;
@@ -23,22 +31,19 @@ textarea {
padding-top: 14px;
padding-bottom: 14px;
- &:hover {
- border-color: $hover-hl;
+ > a:hover {
+ border-bottom-color: transparent !important;
}
- //&-subtitle {
- // font-size: $fs - 2px;
- //}
}
.head-items {
text-align: center;
- padding: 8px 0 16px;
+ padding: 15px 0;
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
- margin-left: -10px;
- margin-right: -10px;
+ //margin-left: -10px;
+ //margin-right: -10px;
max-width: 100%;
box-sizing: border-box;
}
@@ -50,13 +55,6 @@ a.head-item:last-child > span {
border-right: 0;
}
-// blog
-.blog-tags {
- display: none;
-}
-.blog-list.withtags {
- margin-right: 0;
-}
.blog-post-text code {
word-wrap: break-word;
}
diff --git a/htdocs/scss/colors/dark.scss b/htdocs/scss/colors/dark.scss
index 726210e..9c416dc 100644
--- a/htdocs/scss/colors/dark.scss
+++ b/htdocs/scss/colors/dark.scss
@@ -6,6 +6,8 @@ $link-color-underline: #69849d;
$hover-hl: rgba(255, 255, 255, 0.09);
$hover-hl-darker: rgba(255, 255, 255, 0.12);
$grey: #798086;
+$quote_color: #3c9577;
+$quote_line: #45544d;
$dark-grey: $grey;
$light-grey: $grey;
$fg: #eee;
@@ -15,11 +17,13 @@ $code-block-bg: #394146;
$inline-code-block-bg: #394146;
$light-bg: #464c4e;
+$light-bg-hover: #dce3e8;
$dark-bg: #444;
$dark-fg: #999;
$input-border: #48535a;
-$input-border-focused: #48535a;
+$input-border-focused: lighten($input-border, 7%);
+
$input-bg: #30373b;
$border-color: #48535a;
diff --git a/htdocs/scss/colors/light.scss b/htdocs/scss/colors/light.scss
index e670640..33cf07d 100644
--- a/htdocs/scss/colors/light.scss
+++ b/htdocs/scss/colors/light.scss
@@ -6,6 +6,8 @@ $link-color-underline: #95b5da;
$hover-hl: #f0f0f0;
$hover-hl-darker: #ebebeb;
$grey: #888;
+$quote_color: #1f9329;
+$quote_line: #d1e0d2;
$dark-grey: #777;
$light-grey: #999;
$fg: #222;
@@ -15,11 +17,13 @@ $code-block-bg: #f3f3f3;
$inline-code-block-bg: #f1f1f1;
$light-bg: #efefef;
+$light-bg-hover: #dce3e8;
$dark-bg: #dfdfdf;
$dark-fg: #999;
$input-border: #e0e0e0;
-$input-border-focused: #e0e0e0;
+$input-border-focused: darken($input-border, 7%);
+
$input-bg: #f7f7f7;
$border-color: #e0e0e0;
diff --git a/init.php b/init.php
index b139624..a3b58cd 100644
--- a/init.php
+++ b/init.php
@@ -15,17 +15,22 @@ set_include_path(get_include_path().PATH_SEPARATOR.APP_ROOT);
spl_autoload_register(function($class) {
static $libs = [
- 'lib/tags' => ['Tag', 'tags'],
'lib/pages' => ['Page', 'pages'],
- 'lib/posts' => ['Post', 'posts'],
+ 'lib/posts' => ['Post', 'PostText', 'PostLanguage', 'posts'],
'lib/uploads' => ['Upload', 'uploads'],
'engine/model' => ['model'],
'engine/skin' => ['SkinContext'],
];
- if (str_ends_with($class, 'Handler')) {
- $path = APP_ROOT.'/handler/'.str_replace('\\', '/', $class).'.php';
- } else {
+ $path = null;
+ foreach (['Handler', 'Helper'] as $sfx) {
+ if (str_ends_with($class, $sfx)) {
+ $path = APP_ROOT.'/'.strtolower($sfx).'/'.str_replace('\\', '/', $class).'.php';
+ break;
+ }
+ }
+
+ if (is_null($path)) {
foreach ($libs as $lib_file => $class_names) {
if (in_array($class, $class_names)) {
$path = APP_ROOT.'/'.$lib_file.'.php';
@@ -34,7 +39,7 @@ spl_autoload_register(function($class) {
}
}
- if (!isset($path))
+ if (is_null($path))
$path = APP_ROOT.'/lib/'.$class.'.php';
if (!is_file($path))
diff --git a/lib/admin.php b/lib/admin.php
index 116ee3c..a929156 100644
--- a/lib/admin.php
+++ b/lib/admin.php
@@ -4,48 +4,148 @@ require_once 'lib/stored_config.php';
const ADMIN_SESSION_TIMEOUT = 86400 * 14;
const ADMIN_COOKIE_NAME = 'admin_key';
+const ADMIN_LOGIN_MAX_LENGTH = 32;
+$AdminSession = [
+ 'id' => null,
+ 'auth_id' => 0,
+ 'login' => null,
+];
function is_admin(): bool {
- static $is_admin = null;
- if (is_null($is_admin))
- $is_admin = _admin_verify_key();
- return $is_admin;
+ global $AdminSession;
+ if ($AdminSession['id'] === null)
+ _admin_check();
+ return $AdminSession['id'] != 0;
}
-function _admin_verify_key(): bool {
- if (isset($_COOKIE[ADMIN_COOKIE_NAME])) {
- $cookie = (string)$_COOKIE[ADMIN_COOKIE_NAME];
- if ($cookie !== _admin_get_key())
- admin_unset_cookie();
- return true;
- }
- return false;
+function admin_current_info(): array {
+ global $AdminSession;
+ return [
+ 'id' => $AdminSession['id'],
+ 'login' => $AdminSession['login']
+ ];
}
-function admin_check_password(string $pwd): bool {
- return salt_password($pwd) === scGet('admin_pwd');
+function _admin_check(): void {
+ if (!isset($_COOKIE[ADMIN_COOKIE_NAME]))
+ return;
+
+ $cookie = (string)$_COOKIE[ADMIN_COOKIE_NAME];
+ $db = DB();
+ $q = $db->query("SELECT
+ admin_auth.id AS auth_id,
+ admin_auth.admin_id AS id,
+ admins.login AS login
+ FROM admin_auth
+ LEFT JOIN admins ON admin_auth.admin_id=admins.id
+ WHERE admin_auth.token=?
+ LIMIT 1", $cookie);
+
+ if (!$db->numRows($q))
+ return;
+
+ $info = $db->fetch($q);
+
+ global $AdminSession;
+ $AdminSession['id'] = (int)$info['id'];
+ $AdminSession['login'] = $info['login'];
+ $AdminSession['auth_id'] = (int)$info['auth_id'];
}
-function _admin_get_key(): string {
- $admin_pwd_hash = scGet('admin_pwd');
- return salt_password("$admin_pwd_hash|{$_SERVER['REMOTE_ADDR']}");
+function admin_exists(string $login): bool {
+ $db = DB();
+ return (int)$db->result($db->query("SELECT COUNT(*) FROM admins WHERE login=? LIMIT 1", $login)) > 0;
}
-function admin_set_cookie(): void {
+function admin_add(string $login, string $password): int {
+ $db = DB();
+ $db->insert('admins', [
+ 'login' => $login,
+ 'password' => salt_password($password)
+ ]);
+ return $db->insertId();
+}
+
+function admin_delete(string $login): bool {
+ $db = DB();
+ $id = admin_get_id_by_login($login);
+ if (!$db->query("DELETE FROM admins WHERE login=?", $login)) return false;
+ if (!$db->query("DELETE FROM admin_auth WHERE admin_id=?", $id)) return false;
+ return true;
+}
+
+function admin_get_id_by_login(string $login): ?int {
+ $db = DB();
+ $q = $db->query("SELECT id FROM admins WHERE login=?", $login);
+ return $db->numRows($q) > 0 ? (int)$db->result($q) : null;
+}
+
+
+function admin_set_password(string $login, string $password): bool {
+ $db = DB();
+ $db->query("UPDATE admins SET password=? WHERE login=?", salt_password($password), $login);
+ return $db->affectedRows() > 0;
+}
+
+function admin_auth(string $login, string $password): bool {
+ global $AdminSession;
+
+ $db = DB();
+ $q = $db->query("SELECT id FROM admins WHERE login=? AND password=?", $login, salt_password($password));
+ if (!$db->numRows($q))
+ return false;
+
+ $id = (int)$db->result($q);
+ $time = time();
+
+ do {
+ $token = strgen(32);
+ } while ($db->numRows($db->query("SELECT id FROM admin_auth WHERE token=? LIMIT 1", $token)) > 0);
+
+ $db->insert('admin_auth', [
+ 'admin_id' => $id,
+ 'token' => $token,
+ 'ts' => $time
+ ]);
+
+ $db->insert('admin_log', [
+ 'admin_id' => $id,
+ 'ts' => $time,
+ 'ip' => ip2ulong($_SERVER['REMOTE_ADDR']),
+ 'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
+ ]);
+
+ $AdminSession = [
+ 'id' => $id,
+ 'login' => $login,
+ ];
+
+ admin_set_cookie($token);
+ return true;
+}
+
+function admin_logout() {
+ if (!is_admin())
+ return;
+
+ global $AdminSession;
+ $db = DB();
+
+ $db->query("DELETE FROM admin_auth WHERE id=?", $AdminSession['auth_id']);
+
+ $AdminSession['id'] = null;
+ $AdminSession['login'] = null;
+ $AdminSession['auth_id'] = 0;
+
+ admin_unset_cookie();
+}
+
+function admin_set_cookie(string $token): void {
global $config;
- $key = _admin_get_key();
- setcookie(ADMIN_COOKIE_NAME, $key, time() + ADMIN_SESSION_TIMEOUT, '/', $config['cookie_host']);
+ setcookie(ADMIN_COOKIE_NAME, $token, time() + ADMIN_SESSION_TIMEOUT, '/', $config['cookie_host']);
}
function admin_unset_cookie(): void {
global $config;
setcookie(ADMIN_COOKIE_NAME, '', 1, '/', $config['cookie_host']);
}
-
-function admin_log_auth(): void {
- DB()->insert('admin_log', [
- 'ts' => time(),
- 'ip' => ip2ulong($_SERVER['REMOTE_ADDR']),
- 'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
- ]);
-}
diff --git a/lib/cli.php b/lib/cli.php
index 910f4c1..624a719 100644
--- a/lib/cli.php
+++ b/lib/cli.php
@@ -2,11 +2,7 @@
class cli {
- protected ?array $commandsCache = null;
-
- function __construct(
- protected string $ns
- ) {}
+ protected array $commands = [];
protected function usage($error = null): void {
global $argv;
@@ -15,19 +11,15 @@ class cli {
echo "error: {$error}\n\n";
echo "Usage: $argv[0] COMMAND\n\nCommands:\n";
- foreach ($this->getCommands() as $c)
+ foreach ($this->commands as $c => $tmp)
echo " $c\n";
exit(is_null($error) ? 0 : 1);
}
- function getCommands(): array {
- if (is_null($this->commandsCache)) {
- $funcs = array_filter(get_defined_functions()['user'], fn(string $f) => str_starts_with($f, $this->ns));
- $funcs = array_map(fn(string $f) => str_replace('_', '-', substr($f, strlen($this->ns.'\\'))), $funcs);
- $this->commandsCache = array_values($funcs);
- }
- return $this->commandsCache;
+ function on(string $command, callable $f) {
+ $this->commands[$command] = $f;
+ return $this;
}
function run(): void {
@@ -39,12 +31,14 @@ class cli {
if ($argc < 2)
$this->usage();
+ if (empty($this->commands))
+ cli::die("no commands added");
+
$func = $argv[1];
- if (!in_array($func, $this->getCommands()))
+ if (!isset($this->commands[$func]))
self::usage('unknown command "'.$func.'"');
- $func = str_replace('-', '_', $func);
- call_user_func($this->ns.'\\'.$func);
+ $this->commands[$func]();
}
public static function input(string $prompt): string {
diff --git a/lib/posts.php b/lib/posts.php
index 8ffb92b..3149b8e 100644
--- a/lib/posts.php
+++ b/lib/posts.php
@@ -1,28 +1,132 @@
$title,
+ 'lang' => $lang->value,
+ 'post_id' => $this->id,
+ 'html' => $html,
+ 'text' => $text,
+ 'md' => $md,
+ 'toc' => $toc,
+ ];
+
+ $db = DB();
+ if (!$db->insert('posts_texts', $data))
+ return null;
+
+ $id = $db->insertId();
+
+ $post_text = posts::getText($id);
+ $post_text->updateImagePreviews();
+
+ return $post_text;
+ }
+
+ public function registerText(PostText $postText): void {
+ if (array_key_exists($postText->lang->value, $this->texts))
+ throw new Exception("text for language {$postText->lang->value} has already been registered");
+ $this->texts[$postText->lang->value] = $postText;
+ }
+
+ public function loadTexts() {
+ if (!empty($this->texts))
+ return;
+ $db = DB();
+ $q = $db->query("SELECT * FROM posts_texts WHERE post_id=?", $this->id);
+ while ($row = $db->fetch($q)) {
+ $text = new PostText($row);
+ $this->registerText($text);
+ }
+ }
+
+ /**
+ * @return PostText[]
+ */
+ public function getTexts(): array {
+ $this->loadTexts();
+ return $this->texts;
+ }
+
+ public function getText(PostLanguage $lang): ?PostText {
+ $this->loadTexts();
+ return $this->texts[$lang->value] ?? null;
+ }
+
+ public function hasLang(PostLanguage $lang) {
+ $this->loadTexts();
+ foreach ($this->texts as $text) {
+ if ($text->lang == $lang)
+ return true;
+ }
+ return false;
+ }
+
+ public function getUrl(?PostLanguage $lang = null): string {
+ $buf = $this->shortName != '' ? "/articles/{$this->shortName}/" : "/articles/{$this->id}/";
+ if ($lang && $lang != PostLanguage::English)
+ $buf .= '?lang='.$lang->value;
+ return $buf;
+ }
+
+ protected function getTimestamp(): int {
+ return (new DateTime($this->date))->getTimestamp();
+ }
+
+ public function getDate(): string {
+ return date('j M', $this->getTimestamp());
+ }
+
+ public function getYear(): int {
+ return (int)date('Y', $this->getTimestamp());
+ }
+
+ public function getFullDate(): string {
+ return date('j F Y', $this->getTimestamp());
+ }
+
+ public function getDateForInputField(): string {
+ return date('Y-m-d', $this->getTimestamp());
+ }
+}
+
+class PostText extends model {
+ const DB_TABLE = 'posts_texts';
+
+ public int $id;
+ public int $postId;
+ public PostLanguage $lang;
public string $title;
public string $md;
public string $html;
- public string $tocHtml;
public string $text;
- public int $ts;
- public int $updateTs;
- public bool $visible;
public bool $toc;
- public string $shortName;
-
- function edit(array $fields) {
- $cur_ts = time();
- if (!$this->visible && $fields['visible'])
- $fields['ts'] = $cur_ts;
-
- $fields['update_ts'] = $cur_ts;
+ public string $tocHtml;
+ public function edit(array $fields) {
if ($fields['md'] != $this->md) {
$fields['html'] = markup::markdownToHtml($fields['md']);
$fields['text'] = markup::htmlToText($fields['html']);
@@ -36,116 +140,42 @@ class Post extends model {
$this->updateImagePreviews();
}
- function updateHtml() {
+ public function updateHtml(): void {
$html = markup::markdownToHtml($this->md);
$this->html = $html;
-
- DB()->query("UPDATE posts SET html=? WHERE id=?", $html, $this->id);
+ DB()->query("UPDATE posts_texts SET html=? WHERE id=?", $html, $this->id);
}
- function updateText() {
+ public function updateText(): void {
$html = markup::markdownToHtml($this->md);
$text = markup::htmlToText($html);
$this->text = $text;
-
- DB()->query("UPDATE posts SET text=? WHERE id=?", $text, $this->id);
+ DB()->query("UPDATE posts_texts SET text=? WHERE id=?", $text, $this->id);
}
- function getDescriptionPreview(int $len): string {
+ public function getDescriptionPreview(int $len): string {
if (mb_strlen($this->text) >= $len)
return mb_substr($this->text, 0, $len-3).'...';
return $this->text;
}
- function getFirstImage(): ?Upload {
+ public function getFirstImage(): ?Upload {
if (!preg_match('/\{image:([\w]{8})/', $this->md, $match))
return null;
return uploads::getUploadByRandomId($match[1]);
}
- function getUrl(): string {
- return $this->shortName != '' ? "/{$this->shortName}/" : "/{$this->id}/";
- }
-
- function getDate(): string {
- return date('j M', $this->ts);
- }
-
- function getYear(): int {
- return (int)date('Y', $this->ts);
- }
-
- function getFullDate(): string {
- return date('j F Y', $this->ts);
- }
-
- function getUpdateDate(): string {
- return date('j M', $this->updateTs);
- }
-
- function getFullUpdateDate(): string {
- return date('j F Y', $this->updateTs);
- }
-
- function getHtml(bool $is_retina, string $theme): string {
+ public function getHtml(bool $is_retina, string $theme): string {
$html = $this->html;
- $html = markup::htmlImagesFix($html, $is_retina, $theme);
- return $html;
+ return markup::htmlImagesFix($html, $is_retina, $theme);
}
- function getToc(): ?string {
+ public function getTableOfContentsHtml(): ?string {
return $this->toc ? $this->tocHtml : null;
}
- function isUpdated(): bool {
- return $this->updateTs && $this->updateTs != $this->ts;
- }
-
- /**
- * @return Tag[]
- */
- function getTags(): array {
- $db = DB();
- $q = $db->query("SELECT tags.* FROM posts_tags
- LEFT JOIN tags ON tags.id=posts_tags.tag_id
- WHERE posts_tags.post_id=?
- ORDER BY posts_tags.tag_id", $this->id);
- return array_map('Tag::create_instance', $db->fetchAll($q));
- }
-
- /**
- * @return int[]
- */
- function getTagIds(): array {
- $ids = [];
- $db = DB();
- $q = $db->query("SELECT tag_id FROM posts_tags WHERE post_id=? ORDER BY tag_id", $this->id);
- while ($row = $db->fetch($q)) {
- $ids[] = (int)$row['tag_id'];
- }
- return $ids;
- }
-
- function setTagIds(array $new_tag_ids) {
- $cur_tag_ids = $this->getTagIds();
- $add_tag_ids = array_diff($new_tag_ids, $cur_tag_ids);
- $rm_tag_ids = array_diff($cur_tag_ids, $new_tag_ids);
-
- $db = DB();
- if (!empty($add_tag_ids)) {
- $rows = [];
- foreach ($add_tag_ids as $id)
- $rows[] = ['post_id' => $this->id, 'tag_id' => $id];
- $db->multipleInsert('posts_tags', $rows);
- }
-
- if (!empty($rm_tag_ids))
- $db->query("DELETE FROM posts_tags WHERE post_id=? AND tag_id IN(".implode(',', $rm_tag_ids).")", $this->id);
-
- $upd_tag_ids = array_merge($new_tag_ids, $rm_tag_ids);
- $upd_tag_ids = array_unique($upd_tag_ids);
- foreach ($upd_tag_ids as $id)
- tags::recountTagPosts($id);
+ public function hasTableOfContents(): bool {
+ return $this->toc;
}
/**
@@ -153,7 +183,7 @@ class Post extends model {
* @return int
* @throws Exception
*/
- function updateImagePreviews(bool $update = false): int {
+ public function updateImagePreviews(bool $update = false): int {
$images = [];
if (!preg_match_all('/\{image:([\w]{8}),(.*?)}/', $this->md, $matches))
return 0;
@@ -204,80 +234,53 @@ class posts {
return (int)$db->result($db->query($sql));
}
- static function getCountByTagId(int $tag_id, bool $include_hidden = false): int {
- $db = DB();
- if ($include_hidden) {
- $sql = "SELECT COUNT(*) FROM posts_tags WHERE tag_id=?";
- } else {
- $sql = "SELECT COUNT(*) FROM posts_tags
- LEFT JOIN posts ON posts.id=posts_tags.post_id
- WHERE posts_tags.tag_id=? AND posts.visible=1";
- }
- return (int)$db->result($db->query($sql, $tag_id));
- }
-
/**
* @return Post[]
*/
- static function getList(int $offset = 0, int $count = -1, bool $include_hidden = false): array {
+ static function getList(int $offset = 0,
+ int $count = -1,
+ bool $include_hidden = false,
+ ?PostLanguage $filter_by_lang = null
+ ): array {
$db = DB();
$sql = "SELECT * FROM posts";
if (!$include_hidden)
$sql .= " WHERE visible=1";
- $sql .= " ORDER BY ts DESC";
+ $sql .= " ORDER BY `date` DESC";
if ($offset != 0 && $count != -1)
$sql .= "LIMIT $offset, $count";
$q = $db->query($sql);
- return array_map('Post::create_instance', $db->fetchAll($q));
+ $posts = [];
+ while ($row = $db->fetch($q)) {
+ $posts[$row['id']] = $row;
+ }
+
+ if (!empty($posts)) {
+ foreach ($posts as &$post)
+ $post = new Post($post);
+ $q = $db->query("SELECT * FROM posts_texts WHERE post_id IN (".implode(',', array_keys($posts)).")");
+ while ($row = $db->fetch($q)) {
+ $posts[$row['post_id']]->registerText(new PostText($row));
+ }
+ }
+
+ if ($filter_by_lang !== null)
+ $posts = array_filter($posts, fn(Post $post) => $post->hasLang($filter_by_lang));
+
+ return array_values($posts);
}
- /**
- * @return Post[]
- */
- static function getPostsByTagId(int $tag_id, bool $include_hidden = false): array {
+ static function add(array $data = []): ?Post {
$db = DB();
- $sql = "SELECT posts.* FROM posts_tags
- LEFT JOIN posts ON posts.id=posts_tags.post_id
- WHERE posts_tags.tag_id=?";
- if (!$include_hidden)
- $sql .= " AND posts.visible=1";
- $sql .= " ORDER BY posts.ts DESC";
- $q = $db->query($sql, $tag_id);
- return array_map('Post::create_instance', $db->fetchAll($q));
- }
-
- static function add(array $data = []): int|bool {
- $db = DB();
-
- $html = \markup::markdownToHtml($data['md']);
- $text = \markup::htmlToText($html);
-
- $data += [
- 'ts' => time(),
- 'html' => $html,
- 'text' => $text,
- ];
-
if (!$db->insert('posts', $data))
- return false;
-
- $id = $db->insertId();
-
- $post = self::get($id);
- $post->updateImagePreviews();
-
- return $id;
+ return null;
+ return self::get($db->insertId());
}
static function delete(Post $post): void {
- $tags = $post->getTags();
-
$db = DB();
$db->query("DELETE FROM posts WHERE id=?", $post->id);
- $db->query("DELETE FROM posts_tags WHERE post_id=?", $post->id);
-
- foreach ($tags as $tag)
- tags::recountTagPosts($tag->id);
+ $db->query("DELETE FROM posts_texts WHERE post_id=?", $post->id);
}
static function get(int $id): ?Post {
@@ -286,6 +289,12 @@ class posts {
return $db->numRows($q) ? new Post($db->fetch($q)) : null;
}
+ static function getText(int $text_id): ?PostText {
+ $db = DB();
+ $q = $db->query("SELECT * FROM posts_texts WHERE id=?", $text_id);
+ return $db->numRows($q) ? new PostText($db->fetch($q)) : null;
+ }
+
static function getByName(string $short_name): ?Post {
$db = DB();
$q = $db->query("SELECT * FROM posts WHERE short_name=?", $short_name);
diff --git a/lib/tags.php b/lib/tags.php
deleted file mode 100644
index ecc9e5a..0000000
--- a/lib/tags.php
+++ /dev/null
@@ -1,87 +0,0 @@
-tag.'/';
- }
-
- function getPostsCount(bool $is_admin): int {
- return $is_admin ? $this->postsCount : $this->visiblePostsCount;
- }
-
- function __toString(): string {
- return $this->tag;
- }
-
-}
-
-class tags {
-
- static function getAll(bool $include_hidden = false): array {
- $db = DB();
- $field = $include_hidden ? 'posts_count' : 'visible_posts_count';
- $q = $db->query("SELECT * FROM tags WHERE $field > 0 ORDER BY $field DESC, tag");
- return array_map('Tag::create_instance', $db->fetchAll($q));
- }
-
- static function get(string $tag): ?Tag {
- $db = DB();
- $q = $db->query("SELECT * FROM tags WHERE tag=?", $tag);
- return $db->numRows($q) ? new Tag($db->fetch($q)) : null;
- }
-
- static function recountTagPosts(int $tag_id): void {
- $db = DB();
- $count = $db->result($db->query("SELECT COUNT(*) FROM posts_tags WHERE tag_id=?", $tag_id));
- $vis_count = $db->result($db->query("SELECT COUNT(*) FROM posts_tags
- LEFT JOIN posts ON posts.id=posts_tags.post_id
- WHERE posts_tags.tag_id=? AND posts.visible=1", $tag_id));
- $db->query("UPDATE tags SET posts_count=?, visible_posts_count=? WHERE id=?",
- $count, $vis_count, $tag_id);
- }
-
- static function splitString(string $tags): array {
- $tags = trim($tags);
- if ($tags == '')
- return [];
- $tags = preg_split('/,\s+/', $tags);
- $tags = array_filter($tags, static function($tag) { return trim($tag) != ''; });
- $tags = array_map('trim', $tags);
- $tags = array_map('mb_strtolower', $tags);
-
- return $tags;
- }
-
- static function getTags(array $tags): array {
- $found_tags = [];
- $map = [];
-
- $db = DB();
- $q = $db->query("SELECT id, tag FROM tags
- WHERE tag IN ('".implode("','", array_map(fn($tag) => $db->escape($tag), $tags))."')");
-
- while ($row = $db->fetch($q)) {
- $found_tags[] = $row['tag'];
- $map[$row['tag']] = (int)$row['id'];
- }
-
- $notfound_tags = array_diff($tags, $found_tags);
- if (!empty($notfound_tags)) {
- foreach ($notfound_tags as $tag) {
- $db->insert('tags', ['tag' => $tag]);
- $map[$tag] = $db->insertId();
- }
- }
-
- return $map;
- }
-
-}
\ No newline at end of file
diff --git a/lib/uploads.php b/lib/uploads.php
index 3690086..70830db 100644
--- a/lib/uploads.php
+++ b/lib/uploads.php
@@ -1,5 +1,7 @@
randomId.'/'.$this->name;
$updated = false;
- foreach (themes::getThemes() as $theme) {
+ foreach (getThemes() as $theme) {
if (!$may_have_alpha && $theme == 'dark')
continue;
@@ -273,7 +275,7 @@ class Upload extends model {
}
$img = imageopen($orig);
- imageresize($img, $dw, $dh, themes::getThemeAlphaColorAsRGB($theme));
+ imageresize($img, $dw, $dh, getThemeAlphaColorAsRGB($theme));
imagejpeg($img, $dst, $mult == 1 ? 93 : 67);
imagedestroy($img);
diff --git a/routes.php b/routes.php
index 55de520..24019e6 100644
--- a/routes.php
+++ b/routes.php
@@ -3,28 +3,25 @@
return (function() {
$routes = [
'Main' => [
- '/' => 'index',
- '{about,contacts}/' => 'about',
- 'feed.rss' => 'rss',
- '([a-z0-9-]+)/' => 'auto name=$(1)',
+ '/' => 'index',
+ '{about,contacts}/' => 'about',
+ 'feed.rss' => 'rss',
+ '([a-z0-9-]+)/' => 'page name=$(1)',
+ 'articles/' => 'articles',
+ 'articles/([a-z0-9-]+)/' => 'post name=$(1)',
],
'Admin' => [
'admin/' => 'index',
'admin/{login,logout,log}/' => '${1}',
- '([a-z0-9-]+)/{delete,edit}/' => 'auto_${1} short_name=$(1)',
+ '([a-z0-9-]+)/{delete,edit}/' => 'page_${1} short_name=$(1)',
'([a-z0-9-]+)/create/' => 'page_add short_name=$(1)',
+ 'articles/write/' => 'post_add',
+ 'articles/([a-z0-9-]+)/{delete,edit}/' => 'post_${1} short_name=$(1)',
'admin/markdown-preview.ajax' => 'ajax_md_preview',
'admin/uploads/' => 'uploads',
'admin/uploads/{edit_note,delete}/(\d+)/' => 'upload_${1} id=$(1)'
]
];
- if (is_dev()) {
- $routes['Main'] += [
- 'articles/' => 'articles'
- ];
- $routes['Admin'] += [
- 'articles/write/' => 'post_add',
- ];
- }
+
return $routes;
})();
diff --git a/skin/admin.phps b/skin/admin.phps
index 811ca31..ada8617 100644
--- a/skin/admin.phps
+++ b/skin/admin.phps
@@ -11,16 +11,25 @@ function login($ctx) {
$html = <<
+
+
+
+
@@ -37,9 +46,10 @@ return [$html, $js];
// index page
// ----------
-function index($ctx) {
+function index($ctx, $admin_login) {
return <<
+ Authorized as {$admin_login}
Uploads
Sign out
@@ -55,6 +65,11 @@ function uploads($ctx, $uploads, $error) {
return <<if_true($error, $ctx->formError, $error)}
+{$ctx->bc([
+ ['text' => $ctx->lang('admin_title'), 'url' => '/admin/'],
+ ['text' => $ctx->lang('blog_uploads')],
+])}
+