multilang articles support
This commit is contained in:
parent
7fcc080732
commit
ac5a798bd3
@ -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:
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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('/(?<!^)([A-Z])/', ' $1', $http_code->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('/(?<!^)([A-Z])/', ' $1', $http_code->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;
|
||||
|
@ -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 .= '<a class="bc-item" href="'.htmlescape($i['url']).'">';
|
||||
else
|
||||
$buf .= '<span class="bc-item">';
|
||||
$buf .= htmlescape($i['text']);
|
||||
|
||||
if ($has_url)
|
||||
$buf .= ' <span class="bc-arrow">›</span></a>';
|
||||
else
|
||||
$buf .= '</span>';
|
||||
|
||||
return $buf;
|
||||
}, $items));
|
||||
return '<div class="bc"'.($style ? ' style="'.$style.'"' : '').'>'.$buf.'</div>';
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
19
helper/ArticlesHelper.php
Normal file
19
helper/ArticlesHelper.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
class ArticlesHelper {
|
||||
|
||||
static function getRequestedLanguage(): array {
|
||||
list($lang) = input('lang');
|
||||
$pl = null;
|
||||
$default_explicitly_requested = false;
|
||||
if ($lang) {
|
||||
$pl = PostLanguage::tryFrom($lang);
|
||||
if ($pl && $pl == PostLanguage::getDefault())
|
||||
$default_explicitly_requested = true;
|
||||
}
|
||||
if (!$pl)
|
||||
$pl = PostLanguage::getDefault();
|
||||
return [$pl, $default_explicitly_requested];
|
||||
}
|
||||
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 164 B |
Binary file not shown.
Before Width: | Height: | Size: 386 B |
@ -1 +1,2 @@
|
||||
var LS = window.localStorage;
|
||||
window.cur = {};
|
@ -1,29 +1,32 @@
|
||||
var Draft = {
|
||||
get: function() {
|
||||
if (!LS) return null;
|
||||
|
||||
var title = LS.getItem('draft_title') || null;
|
||||
var text = LS.getItem('draft_text') || null;
|
||||
|
||||
return {
|
||||
title: title,
|
||||
text: text
|
||||
};
|
||||
},
|
||||
|
||||
setTitle: function(text) {
|
||||
if (!LS) return null;
|
||||
LS.setItem('draft_title', text);
|
||||
},
|
||||
|
||||
setText: function(text) {
|
||||
if (!LS) return null;
|
||||
LS.setItem('draft_text', text);
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
if (!LS) return;
|
||||
LS.removeItem('draft_title');
|
||||
LS.removeItem('draft_text');
|
||||
class Draft {
|
||||
constructor(id, lang = 'en') {
|
||||
this.id = id
|
||||
this.lang = lang
|
||||
}
|
||||
};
|
||||
|
||||
setLang(lang) {
|
||||
this.lang = lang
|
||||
}
|
||||
|
||||
getForLang(lang, what) {
|
||||
return LS.getItem(this.key(what, lang)) || ''
|
||||
}
|
||||
|
||||
key(what, lang = null) {
|
||||
if (lang === null)
|
||||
lang = this.lang
|
||||
return `draft_${this.id}_${what}__${lang}`
|
||||
}
|
||||
|
||||
reset(langs) {
|
||||
for (const what of ['title', 'text']) {
|
||||
for (const l of langs)
|
||||
LS.removeItem(this.key(what, l))
|
||||
}
|
||||
}
|
||||
|
||||
get title() { return LS.getItem(this.key('title')) || '' }
|
||||
get text() { return LS.getItem(this.key('text')) || '' }
|
||||
set title(val) { LS.setItem(this.key('title'), val) }
|
||||
set text(val) { LS.setItem(this.key('text'), val) }
|
||||
}
|
||||
|
@ -1,101 +1,211 @@
|
||||
var AdminWriteForm = {
|
||||
form: null,
|
||||
previewTimeout: null,
|
||||
previewRequest: null,
|
||||
class AdminWriteEditForm {
|
||||
constructor(opts = {}) {
|
||||
this.opts = opts
|
||||
this.form = document.forms[this.isPage() ? 'pageForm' : 'postForm']
|
||||
this.isFixed = false
|
||||
this.previewTimeout = null
|
||||
this.previewRequest = null
|
||||
this.tocByLang = {}
|
||||
|
||||
init: function(opts) {
|
||||
opts = opts || {};
|
||||
|
||||
this.opts = opts;
|
||||
this.form = document.forms[opts.pages ? 'pageForm' : 'postForm'];
|
||||
this.isFixed = false;
|
||||
|
||||
addEvent(this.form, 'submit', this.onSubmit);
|
||||
if (!opts.pages)
|
||||
addEvent(this.form.title, 'input', this.onInput);
|
||||
|
||||
addEvent(this.form.text, 'input', this.onInput);
|
||||
addEvent(ge('toggle_wrap'), 'click', this.onToggleWrapClick);
|
||||
|
||||
if (this.form.text.value !== '')
|
||||
this.showPreview();
|
||||
|
||||
// TODO make it more clever and context-aware
|
||||
/*var draft = Draft.get();
|
||||
if (draft.title)
|
||||
this.form.title.value = draft.title;
|
||||
if (draft.text)
|
||||
this.form.text.value = draft.text;*/
|
||||
|
||||
addEvent(window, 'scroll', this.onScroll);
|
||||
addEvent(window, 'resize', this.onResize);
|
||||
},
|
||||
|
||||
showPreview: function() {
|
||||
if (this.previewRequest !== null) {
|
||||
this.previewRequest.abort();
|
||||
if (!this.isEditing()) {
|
||||
for (const l of opts.langs) {
|
||||
this.tocByLang[l] = false
|
||||
}
|
||||
}
|
||||
var params = {
|
||||
md: this.form.elements.text.value,
|
||||
use_image_previews: this.opts.pages ? 1 : 0
|
||||
};
|
||||
if (!this.opts.pages)
|
||||
params.title = this.form.elements.title.value
|
||||
this.previewRequest = ajax.post('/admin/markdown-preview.ajax', params, function(err, response) {
|
||||
if (err)
|
||||
return console.error(err);
|
||||
ge('preview_html').innerHTML = response.html;
|
||||
});
|
||||
},
|
||||
|
||||
onSubmit: function(event) {
|
||||
this.form.addEventListener('submit', this.onSubmit)
|
||||
if (this.isPost())
|
||||
this.form.title.addEventListener('input', this.onInput)
|
||||
|
||||
this.form.text.addEventListener('input', this.onInput)
|
||||
ge('toggle_wrap').addEventListener('click', this.onToggleWrapClick)
|
||||
|
||||
if (this.isPost()) {
|
||||
ge('toc_cb').addEventListener('change', (e) => {
|
||||
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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
@ -361,3 +356,37 @@ a.index-dl-line {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
17
init.php
17
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))
|
||||
|
156
lib/admin.php
156
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'] ?? '',
|
||||
]);
|
||||
}
|
||||
|
26
lib/cli.php
26
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 {
|
||||
|
315
lib/posts.php
315
lib/posts.php
@ -1,28 +1,132 @@
|
||||
<?php
|
||||
|
||||
enum PostLanguage: string {
|
||||
case Russian = 'ru';
|
||||
case English = 'en';
|
||||
|
||||
public static function getDefault(): PostLanguage {
|
||||
return self::English;
|
||||
}
|
||||
}
|
||||
|
||||
class Post extends model {
|
||||
|
||||
const DB_TABLE = 'posts';
|
||||
|
||||
public int $id;
|
||||
public string $date;
|
||||
public bool $visible;
|
||||
public string $shortName;
|
||||
|
||||
protected array $texts = [];
|
||||
|
||||
public function addText(PostLanguage $lang, string $title, string $md, bool $toc): ?PostText {
|
||||
$html = markup::markdownToHtml($md);
|
||||
$text = markup::htmlToText($html);
|
||||
|
||||
$data = [
|
||||
'title' => $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);
|
||||
|
87
lib/tags.php
87
lib/tags.php
@ -1,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
class Tag extends model implements Stringable {
|
||||
|
||||
const DB_TABLE = 'tags';
|
||||
|
||||
public int $id;
|
||||
public string $tag;
|
||||
public int $postsCount;
|
||||
public int $visiblePostsCount;
|
||||
|
||||
function getUrl(): string {
|
||||
return '/'.$this->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;
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
require_once 'lib/themes.php';
|
||||
|
||||
const UPLOADS_ALLOWED_EXTENSIONS = [
|
||||
'jpg', 'png', 'git', 'mp4', 'mp3', 'ogg', 'diff', 'txt', 'gz', 'tar',
|
||||
'icc', 'icm', 'patch', 'zip', 'brd', 'pdf', 'lua', 'xpi', 'rar', '7z',
|
||||
@ -255,7 +257,7 @@ class Upload extends model {
|
||||
$orig = $config['uploads_dir'].'/'.$this->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);
|
||||
|
||||
|
23
routes.php
23
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;
|
||||
})();
|
||||
|
@ -11,16 +11,25 @@ function login($ctx) {
|
||||
$html = <<<HTML
|
||||
<form action="/admin/login/" method="post" class="form-layout-h">
|
||||
<input type="hidden" name="token" value="{$ctx->csrf('adminlogin')}" />
|
||||
|
||||
<div class="form-field-wrap clearfix">
|
||||
<div class="form-field-label">{$ctx->lang('as_form_password')}:</div>
|
||||
<div class="form-field-label">{$ctx->lang('admin_login')}:</div>
|
||||
<div class="form-field">
|
||||
<input id="as_password" class="form-field-input" type="password" name="password" size="50" />
|
||||
<input class="form-field-input" type="text" name="login" size="50" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field-wrap clearfix">
|
||||
<div class="form-field-label">{$ctx->lang('admin_password')}:</div>
|
||||
<div class="form-field">
|
||||
<input class="form-field-input" type="password" name="password" size="50" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field-wrap clearfix">
|
||||
<div class="form-field-label"></div>
|
||||
<div class="form-field">
|
||||
<button type="submit">{$ctx->lang('submit')}</button>
|
||||
<button type="submit">{$ctx->lang('sign_in')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@ -37,9 +46,10 @@ return [$html, $js];
|
||||
// index page
|
||||
// ----------
|
||||
|
||||
function index($ctx) {
|
||||
function index($ctx, $admin_login) {
|
||||
return <<<HTML
|
||||
<div class="admin-page">
|
||||
Authorized as <b>{$admin_login}</b><br>
|
||||
<!--<a href="/admin/log/">Log</a><br/>-->
|
||||
<a href="/admin/uploads/">Uploads</a><br>
|
||||
<a href="/admin/logout/?token={$ctx->csrf('logout')}">Sign out</a>
|
||||
@ -55,6 +65,11 @@ function uploads($ctx, $uploads, $error) {
|
||||
return <<<HTML
|
||||
{$ctx->if_true($error, $ctx->formError, $error)}
|
||||
|
||||
{$ctx->bc([
|
||||
['text' => $ctx->lang('admin_title'), 'url' => '/admin/'],
|
||||
['text' => $ctx->lang('blog_uploads')],
|
||||
])}
|
||||
|
||||
<div class="blog-upload-form">
|
||||
<form action="/admin/uploads/" method="post" enctype="multipart/form-data" class="form-layout-h">
|
||||
<input type="hidden" name="token" value="{$ctx->csrf('addupl')}" />
|
||||
@ -121,29 +136,41 @@ return <<<HTML
|
||||
HTML;
|
||||
}
|
||||
|
||||
function postForm($ctx,
|
||||
function postForm(\SkinContext $ctx,
|
||||
string|Stringable $title,
|
||||
string|Stringable $text,
|
||||
string|Stringable $short_name,
|
||||
string|Stringable $tags = '',
|
||||
array $langs,
|
||||
array $js_texts,
|
||||
string|Stringable|null $date = null,
|
||||
bool $is_edit = false,
|
||||
$error_code = null,
|
||||
?bool $saved = null,
|
||||
?bool $visible = null,
|
||||
?bool $toc = null,
|
||||
string|Stringable|null $post_url = null,
|
||||
?int $post_id = null): array {
|
||||
?int $post_id = null,
|
||||
?string $lang = null): array {
|
||||
|
||||
$form_url = !$is_edit ? '/articles/write/' : $post_url.'edit/';
|
||||
|
||||
// breadcrumbs
|
||||
$bc_tree = [
|
||||
['url' => '/articles/?lang='.$lang, 'text' => $ctx->lang('articles')]
|
||||
];
|
||||
if ($is_edit) {
|
||||
$bc_tree[] = ['url' => $post_url.'?lang='.$lang, 'text' => $ctx->lang('blog_view_post')];
|
||||
} else {
|
||||
$bc_tree[] = ['text' => $ctx->lang('blog_new_post')];
|
||||
}
|
||||
|
||||
$html = <<<HTML
|
||||
{$ctx->if_true($error_code, fn() => '<div class="form-error">'.$ctx->lang('err_blog_'.$error_code).'</div>')}
|
||||
<div class="form-error" id="form-error" style="display:none"></div>
|
||||
{$ctx->if_true($saved, fn() => '<div class="form-success">'.$ctx->lang('info_saved').'</div>')}
|
||||
{$ctx->bc($bc_tree, 'padding-bottom: 20px')}
|
||||
<table cellpadding="0" cellspacing="0" class="blog-write-table">
|
||||
<tr>
|
||||
<td id="form_first_cell">
|
||||
<form class="blog-write-form form-layout-v" name="postForm" action="{$form_url}" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="token" value="{$ctx->if_then_else($is_edit, $ctx->csrf('editpost'.$post_id), $ctx->csrf('addpost'))}" />
|
||||
|
||||
<form class="blog-write-form form-layout-v" name="postForm" action="{$form_url}" method="post">
|
||||
<div class="form-field-wrap clearfix">
|
||||
<div class="form-field-label">{$ctx->lang('blog_write_form_title')}</div>
|
||||
<div class="form-field">
|
||||
@ -164,24 +191,34 @@ $html = <<<HTML
|
||||
<tr>
|
||||
<td>
|
||||
<div class="clearfix">
|
||||
<div class="form-field-label">{$ctx->lang('blog_write_form_tags')}</div>
|
||||
<div class="form-field-label">{$ctx->lang('blog_post_options')}</div>
|
||||
<div class="form-field">
|
||||
<input class="form-field-input" type="text" name="tags" value="{$tags}" />
|
||||
<label for="visible_cb"><input type="checkbox" id="visible_cb" name="visible"{$ctx->if_true($visible, ' checked="checked"')}> {$ctx->lang('blog_write_form_visible')}</label>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="clearfix">
|
||||
<div class="form-field-label">{$ctx->lang('blog_write_form_options')}</div>
|
||||
<div class="form-field-label">{$ctx->lang('blog_text_options')}</div>
|
||||
<div class="form-field">
|
||||
<label for="visible_cb"><input type="checkbox" id="visible_cb" name="visible"{$ctx->if_true($visible, ' checked="checked"')}> {$ctx->lang('blog_write_form_visible')}</label>
|
||||
<label for="toc_cb"><input type="checkbox" id="toc_cb" name="toc"{$ctx->if_true($toc, ' checked="checked"')}> {$ctx->lang('blog_write_form_toc')}</label>
|
||||
<select name="lang">
|
||||
{$ctx->for_each($langs, fn($l) => '<option value="'.$l->value.'"'.($l->value == $lang ? ' selected="selected"' : '').'>'.$l->value.'</option>')}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="clearfix">
|
||||
<div class="form-field-label">{$ctx->lang('blog_write_form_date')}</div>
|
||||
<div class="form-field">
|
||||
<input type="date" name="date"{$ctx->if_true($date, ' value="'.$date.'"')}>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<td colspan="2">
|
||||
<div class="clearfix">
|
||||
<div class="form-field-label">{$ctx->lang('blog_write_form_short_name')}</div>
|
||||
<div class="form-field">
|
||||
@ -190,11 +227,9 @@ $html = <<<HTML
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="clearfix">
|
||||
<div class="form-field-label"> </div>
|
||||
<div class="form-field">
|
||||
<button type="submit" name="submit_btn"><b>{$ctx->lang('blog_write_form_submit_btn')}</b></button>
|
||||
</div>
|
||||
<div class="form-field-label"> </div>
|
||||
<div class="form-field">
|
||||
<button type="submit" name="submit_btn"><b>{$ctx->lang($is_edit ? 'save' : 'blog_write_form_submit_btn')}</b></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -210,10 +245,21 @@ $html = <<<HTML
|
||||
</table>
|
||||
HTML;
|
||||
|
||||
$js_params = json_encode($is_edit
|
||||
? ['edit' => true, 'id' => $post_id]
|
||||
: (object)[]);
|
||||
$js = "AdminWriteForm.init({$js_params});";
|
||||
$js_params = [
|
||||
'langs' => array_map(fn($lang) => $lang->value, $langs),
|
||||
'token' => $is_edit ? csrf_get('editpost'.$post_id) : csrf_get('post_add')
|
||||
];
|
||||
if ($is_edit)
|
||||
$js_params += [
|
||||
'edit' => true,
|
||||
'id' => $post_id,
|
||||
'texts' => $js_texts
|
||||
];
|
||||
$js_params = jsonEncode($js_params);
|
||||
|
||||
$js = <<<JAVASCRIPT
|
||||
cur.form = new AdminWriteEditForm({$js_params});
|
||||
JAVASCRIPT;
|
||||
|
||||
return [$html, $js];
|
||||
}
|
||||
|
@ -34,11 +34,9 @@ return <<<HTML
|
||||
</head>
|
||||
<body{$ctx->if_true($body_class, ' class="'.implode(' ', $body_class).'"')}>
|
||||
<div class="page-content base-width">
|
||||
{$ctx->renderHeader($theme)}
|
||||
{$ctx->renderHeader($theme, $opts['head_section'], $opts['articles_lang'], $opts['is_index'])}
|
||||
<div class="page-content-inner">{$unsafe_body}</div>
|
||||
<div class="footer">
|
||||
Email: <a href="mailto:{$admin_email}">{$admin_email}</a>
|
||||
</div>
|
||||
{$ctx->if_not($opts['full_width'], fn() => $ctx->renderFooter($admin_email))}
|
||||
</div>
|
||||
{$ctx->renderScript($js, $unsafe_lang)}
|
||||
|
||||
@ -187,28 +185,36 @@ function getStaticVersion(string $name): string {
|
||||
}
|
||||
|
||||
|
||||
function renderHeader(SkinContext $ctx, string $theme): string {
|
||||
function renderHeader(SkinContext $ctx,
|
||||
string $theme,
|
||||
?string $section,
|
||||
?string $articles_lang,
|
||||
bool $show_subtitle): string {
|
||||
$items = [];
|
||||
if (is_admin())
|
||||
$items[] = ['url' => '/articles/', 'label' => 'articles'];
|
||||
$items[] = ['url' => '/articles/'.($articles_lang ? '?lang='.$articles_lang : ''), 'label' => 'articles', 'selected' => $section === 'articles'];
|
||||
array_push($items,
|
||||
['url' => 'https://files.4in1.ws', 'label' => 'materials'],
|
||||
['url' => '/info/', 'label' => 'about']
|
||||
['url' => 'https://files.4in1.ws', 'label' => 'files', 'selected' => $section === 'files'],
|
||||
['url' => '/info/', 'label' => 'about', 'selected' => $section === 'about']
|
||||
);
|
||||
if (is_admin())
|
||||
$items[] = ['url' => '/admin/', 'label' => $ctx->renderSettingsIcon(), 'type' => 'settings'];
|
||||
$items[] = ['url' => '/admin/', 'label' => $ctx->renderSettingsIcon(), 'type' => 'settings', 'selected' => $section === 'admin'];
|
||||
$items[] = ['url' => 'javascript:void(0)', 'label' => $ctx->renderMoonIcons(), 'type' => 'theme-switcher', 'type_opts' => $theme];
|
||||
|
||||
// here, items are rendered using for_each, so that there are no gaps (whitespaces) between tags
|
||||
|
||||
$class = 'head';
|
||||
if (!$show_subtitle)
|
||||
$class .= ' no-subtitle';
|
||||
|
||||
return <<<HTML
|
||||
<div class="head">
|
||||
<div class="{$class}">
|
||||
<div class="head-inner">
|
||||
<div class="head-logo-wrap">
|
||||
<div class="head-logo">
|
||||
<a href="/">
|
||||
<div class="head-logo-title">4in1 <span class="head-logo-title-author">by idb</span></div>
|
||||
<div class="head-logo-subtitle">Mask of Shakespeare, Mysteries of Bacon,<br>Book by Cartier, Secrets of the NSA</div>
|
||||
{$ctx->if_true($show_subtitle, '<div class="head-logo-subtitle">Mask of Shakespeare, Mysteries of Bacon, <br>Book by Cartier, Secrets of the NSA</div>')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -218,6 +224,7 @@ return <<<HTML
|
||||
$item['label'],
|
||||
$item['type'] ?? false,
|
||||
$item['type_opts'] ?? null,
|
||||
$item['selected'] ?? false
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@ -225,12 +232,12 @@ return <<<HTML
|
||||
HTML;
|
||||
}
|
||||
|
||||
|
||||
function renderHeaderItem(SkinContext $ctx,
|
||||
string $url,
|
||||
?Stringable $unsafe_label,
|
||||
?string $type,
|
||||
?string $type_opts): string {
|
||||
?string $type_opts,
|
||||
bool $selected): string {
|
||||
$args = '';
|
||||
$class = '';
|
||||
switch ($type) {
|
||||
@ -242,6 +249,9 @@ switch ($type) {
|
||||
$class = ' is-settings';
|
||||
break;
|
||||
}
|
||||
if ($selected)
|
||||
$class .= ' is-selected';
|
||||
|
||||
return <<<HTML
|
||||
<a class="head-item{$class}" href="{$url}"{$args}>{$unsafe_label}</a>
|
||||
HTML;
|
||||
@ -272,3 +282,12 @@ return <<<SVG
|
||||
SVG;
|
||||
|
||||
}
|
||||
|
||||
function renderFooter($ctx, $admin_email): string {
|
||||
return <<<HTML
|
||||
<div class="footer">
|
||||
Email: <a href="mailto:{$admin_email}">{$admin_email}</a>
|
||||
</div>
|
||||
HTML;
|
||||
|
||||
}
|
||||
|
148
skin/main.phps
148
skin/main.phps
@ -2,8 +2,12 @@
|
||||
|
||||
namespace skin\main;
|
||||
|
||||
// index page
|
||||
// ----------
|
||||
// articles page
|
||||
// -------------
|
||||
|
||||
use Post;
|
||||
use PostLanguage;
|
||||
use function is_admin;
|
||||
|
||||
function index($ctx) {
|
||||
return <<<HTML
|
||||
@ -39,52 +43,48 @@ HTML;
|
||||
|
||||
}
|
||||
|
||||
//function articles($ctx): string {
|
||||
//return <<<HTML
|
||||
//<div class="empty">
|
||||
// {$ctx->lang('blog_no')}
|
||||
// {$ctx->if_admin('<a href="/write/">'.$ctx->lang('write').'</a>')}
|
||||
//</div>
|
||||
//HTML;
|
||||
//}
|
||||
function articles($ctx, array $posts, PostLanguage $selected_lang): string {
|
||||
if (empty($posts))
|
||||
return $ctx->articlesEmpty($selected_lang);
|
||||
|
||||
function articles($ctx, array $posts): string {
|
||||
return <<<HTML
|
||||
<div class="blog-list">
|
||||
<div class="blog-list-title">
|
||||
<!--all posts-->
|
||||
{$ctx->if_admin(
|
||||
'<span>
|
||||
<a href="/articles/write/">new</a>
|
||||
</span>'
|
||||
)}
|
||||
</div>
|
||||
{$ctx->indexPostsTable($posts)}
|
||||
{$ctx->articlesPostsTable($posts, $selected_lang)}
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
function indexPostsTable($ctx, array $posts): string {
|
||||
function articlesEmpty($ctx, PostLanguage $selected_lang) {
|
||||
return <<<HTML
|
||||
<div class="empty">
|
||||
{$ctx->lang('blog_no')}
|
||||
{$ctx->articlesRightLinks($selected_lang->value)}
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
function articlesPostsTable($ctx, array $posts, PostLanguage $selected_lang): string {
|
||||
$ctx->year = 3000;
|
||||
return <<<HTML
|
||||
<div class="blog-list-table-wrap">
|
||||
<table class="blog-list-table" width="100%" cellspacing="0" cellpadding="0">
|
||||
{$ctx->for_each($posts, fn($post) => $ctx->indexPostRow(
|
||||
$post->getYear(),
|
||||
$post->visible,
|
||||
$post->getDate(),
|
||||
$post->getUrl(),
|
||||
$post->title
|
||||
))}
|
||||
{$ctx->for_each($posts, fn($post, $i) => $ctx->articlesPostRow($i, $post, $selected_lang))}
|
||||
</table>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
function indexPostRow($ctx, $year, $is_visible, $date, $url, $title): string {
|
||||
function articlesPostRow($ctx, int $index, Post $post, PostLanguage $selected_lang): string {
|
||||
$year = $post->getYear();
|
||||
$date = $post->getDate();
|
||||
$url = $post->getUrl($selected_lang);
|
||||
|
||||
$pt = $post->getText($selected_lang);
|
||||
$title = $pt->title;
|
||||
|
||||
return <<<HTML
|
||||
{$ctx->if_true($ctx->year > $year, $ctx->indexYearLine, $year)}
|
||||
<tr class="blog-item-row{$ctx->if_not($is_visible, ' ishidden')}">
|
||||
{$ctx->if_true($ctx->year > $year, $ctx->articlesIndexYearLine, $year, $index === 0, $selected_lang->value)}
|
||||
<tr class="blog-item-row{$ctx->if_not($post->visible, ' ishidden')}">
|
||||
<td class="blog-item-date-cell">
|
||||
<span class="blog-item-date">{$date}</span>
|
||||
</td>
|
||||
@ -95,17 +95,43 @@ return <<<HTML
|
||||
HTML;
|
||||
}
|
||||
|
||||
function indexYearLine($ctx, $year): string {
|
||||
function articlesIndexYearLine($ctx, $year, $show_right_links, string $selected_lang): string {
|
||||
$ctx->year = $year;
|
||||
return <<<HTML
|
||||
<tr class="blog-item-row-year">
|
||||
<td class="blog-item-date-cell"><span>{$year}</span></td>
|
||||
<td></td>
|
||||
<td>
|
||||
{$ctx->if_true($show_right_links, $ctx->articlesRightLinks($selected_lang))}
|
||||
</td>
|
||||
</tr>
|
||||
HTML;
|
||||
}
|
||||
|
||||
|
||||
function articlesRightLinks($ctx, string $selected_lang) {
|
||||
$links = [
|
||||
['url' => $selected_lang != 'en' ? '/articles/' : null, 'label' => 'en'],
|
||||
['url' => $selected_lang != 'ru' ? '/articles/?lang=ru' : null, 'label' => 'ru'],
|
||||
];
|
||||
if (is_admin()) {
|
||||
$links[] = ['url' => '/articles/write/?lang='.$selected_lang, 'label' => 'write'];
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<div class="blog-item-right-links">
|
||||
{$ctx->for_each($links, fn($link, $index) => $ctx->articlesRightLink($link['url'], $link['label'], $index))}
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
function articlesRightLink($ctx, $url, string $label, int $index) {
|
||||
$buf = '';
|
||||
if ($index > 0)
|
||||
$buf .= ' <span class="blog-links-separator">|</span> ';
|
||||
$buf .= !$url ? $label : '<a href="'.$url.'">'.$label.'</a>';
|
||||
return $buf;
|
||||
}
|
||||
|
||||
|
||||
// any page
|
||||
// --------
|
||||
@ -113,6 +139,9 @@ HTML;
|
||||
function page($ctx, $page_url, $short_name, $unsafe_html) {
|
||||
$html = <<<HTML
|
||||
<div class="page">
|
||||
<!--<div class="blog-post-title-nav">
|
||||
<a href="/">{$ctx->lang('index')}</a> <span>›</span>
|
||||
</div>-->
|
||||
{$ctx->if_admin($ctx->pageAdminLinks, $page_url, $short_name)}
|
||||
<div class="blog-post-text">{$unsafe_html}</div>
|
||||
</div>
|
||||
@ -135,20 +164,21 @@ HTML;
|
||||
// post page
|
||||
// ---------
|
||||
|
||||
function post($ctx, $id, $title, $unsafe_html, $unsafe_toc_html, $date, $visible, $url, $tags, $email, $urlencoded_reply_subject) {
|
||||
function post($ctx, $id, $title, $unsafe_html, $unsafe_toc_html, $date, $visible, $url, string $lang, $other_langs) {
|
||||
$html = <<<HTML
|
||||
<div class="blog-post-wrap2">
|
||||
<div class="blog-post-wrap1">
|
||||
<div class="blog-post">
|
||||
{$ctx->bc([
|
||||
['url' => '/articles/?lang='.$lang, 'text' => $ctx->lang('articles')]
|
||||
])}
|
||||
<div class="blog-post-title">
|
||||
<h1>{$title}</h1>
|
||||
<div class="blog-post-date">
|
||||
{$ctx->if_not($visible, '<b>'.$ctx->lang('blog_post_hidden').'</b> |')}
|
||||
{$ctx->if_not($visible, $ctx->lang('blog_post_hidden').' |')}
|
||||
{$date}
|
||||
{$ctx->if_admin($ctx->postAdminLinks, $url, $id)}
|
||||
</div>
|
||||
<div class="blog-post-tags clearfix">
|
||||
{$ctx->for_each($tags, fn($tag) => $ctx->postTag($tag->getUrl(), $tag->tag))}
|
||||
{$ctx->if_true($other_langs, $ctx->postOtherLangs($url, $other_langs))}
|
||||
{$ctx->if_admin($ctx->postAdminLinks, $url, $id, $lang)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="blog-post-text">{$unsafe_html}</div>
|
||||
@ -161,6 +191,14 @@ HTML;
|
||||
return [$html, markdownThemeChangeListener()];
|
||||
}
|
||||
|
||||
function postOtherLangs($ctx, $url, $other_langs) {
|
||||
$buf = '';
|
||||
foreach ($other_langs as $lang) {
|
||||
$buf .= ' | <a href="'.$url.'?lang='.$lang.'">'.$ctx->lang('blog_read_in_'.$lang).'</a>';
|
||||
}
|
||||
return $buf;
|
||||
}
|
||||
|
||||
function postToc($ctx, $unsafe_toc_html) {
|
||||
return <<<HTML
|
||||
<div class="blog-post-toc">
|
||||
@ -175,16 +213,10 @@ HTML;
|
||||
|
||||
}
|
||||
|
||||
function postAdminLinks($ctx, $url, $id) {
|
||||
function postAdminLinks($ctx, $url, $id, string $lang) {
|
||||
return <<<HTML
|
||||
<a href="{$url}edit/">{$ctx->lang('edit')}</a>
|
||||
<a href="{$url}delete/?token={$ctx->csrf('delpost'.$id)}" onclick="return confirm('{$ctx->lang('blog_post_delete_confirmation')}')">{$ctx->lang('delete')}</a>
|
||||
HTML;
|
||||
}
|
||||
|
||||
function postTag($ctx, $url, $name) {
|
||||
return <<<HTML
|
||||
<a href="{$url}"><span>#</span>{$name}</a>
|
||||
| <a href="{$url}edit/?lang={$lang}">{$ctx->lang('edit')}</a>
|
||||
| <a href="{$url}delete/?token={$ctx->csrf('delpost'.$id)}" onclick="return confirm('{$ctx->lang('blog_post_delete_confirmation')}')">{$ctx->lang('delete')}</a>
|
||||
HTML;
|
||||
}
|
||||
|
||||
@ -213,23 +245,3 @@ ThemeSwitcher.addOnChangeListener(function(isDark) {
|
||||
});
|
||||
JS;
|
||||
}
|
||||
|
||||
|
||||
// tag page
|
||||
// --------
|
||||
|
||||
function tag($ctx, $count, $posts, $tag) {
|
||||
if (!$count)
|
||||
return <<<HTML
|
||||
<div class="empty">
|
||||
{$ctx->lang('blog_tag_not_found')}
|
||||
</div>
|
||||
HTML;
|
||||
|
||||
return <<<HTML
|
||||
<div class="blog-list">
|
||||
<div class="blog-list-title">#{$tag}</div>
|
||||
{$ctx->indexPostsTable($posts)}
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
@ -1,36 +1,26 @@
|
||||
# common
|
||||
4in1: '4in1'
|
||||
site_title: '4in1. Mask of Shakespeare, mysteries of Bacon, book by Cartier, secrets of the NSA'
|
||||
index_title: '4in1 | Index'
|
||||
index: 'Index'
|
||||
|
||||
posts: 'posts'
|
||||
all_posts: 'all posts'
|
||||
blog: 'blog'
|
||||
contacts: 'contacts'
|
||||
email: 'email'
|
||||
projects: 'projects'
|
||||
posts: 'Posts'
|
||||
all_posts: 'All posts'
|
||||
articles: 'Articles'
|
||||
unknown_error: 'Unknown error'
|
||||
error: 'Error'
|
||||
write: 'Write'
|
||||
submit: 'submit'
|
||||
sign_in: "Sign in"
|
||||
edit: 'edit'
|
||||
delete: 'delete'
|
||||
save: "Save"
|
||||
info_saved: 'Information saved.'
|
||||
toc: 'Table of Contents'
|
||||
|
||||
# theme switcher
|
||||
theme_auto: 'auto'
|
||||
theme_dark: 'dark'
|
||||
theme_light: 'light'
|
||||
|
||||
# contacts
|
||||
contacts_email: 'email'
|
||||
contacts_pgp: 'OpenPGP public key'
|
||||
contacts_tg: 'telegram'
|
||||
contacts_freenode: 'freenode'
|
||||
|
||||
# blog
|
||||
blog_tags: 'tags'
|
||||
blog_new_post: "New post"
|
||||
blog_view_post: "View post"
|
||||
#blog_editing: "Editing..."
|
||||
blog_latest: 'Latest posts'
|
||||
blog_no: 'No posts yet.'
|
||||
blog_view_all: 'View all'
|
||||
@ -38,23 +28,24 @@ blog_write: 'Write a post'
|
||||
blog_post_delete_confirmation: 'Are you sure you want to delete this post?'
|
||||
blog_post_edit_title: 'Edit post "%s"'
|
||||
blog_post_hidden: 'Hidden'
|
||||
blog_tag_title: 'Posts tagged with "%s"'
|
||||
blog_tag_not_found: 'No posts found.'
|
||||
blog_comments_text: 'If you have any comments, <a href="mailto:%s?subject=%s">contact me by email</a>.'
|
||||
blog_read_in_ru: "Читать на русском"
|
||||
blog_read_in_en: "Read in English"
|
||||
|
||||
blog_write_form_preview_btn: 'Preview'
|
||||
blog_write_form_submit_btn: 'Submit'
|
||||
blog_write_form_title: 'Title'
|
||||
blog_write_form_text: 'Text'
|
||||
blog_write_form_date: 'Date'
|
||||
blog_write_form_preview: 'Preview'
|
||||
blog_write_form_enter_text: 'Enter text..'
|
||||
blog_write_form_enter_title: 'Enter title..'
|
||||
blog_write_form_tags: 'Tags'
|
||||
blog_write_form_visible: 'Visible'
|
||||
blog_write_form_toc: 'ToC'
|
||||
blog_write_form_short_name: 'Short name'
|
||||
blog_write_form_toggle_wrap: 'Toggle wrap'
|
||||
blog_write_form_options: 'Options'
|
||||
blog_post_options: "Post options"
|
||||
blog_text_options: "Text options"
|
||||
|
||||
blog_uploads: 'Uploads'
|
||||
blog_upload: 'Upload files'
|
||||
@ -66,9 +57,8 @@ blog_upload_form_custom_name: 'Custom name'
|
||||
blog_upload_form_note: 'Note'
|
||||
|
||||
# blog (errors)
|
||||
err_blog_no_title: 'Title not specified'
|
||||
err_blog_no_text: 'Text not specified'
|
||||
err_blog_no_tags: 'Tags not specified'
|
||||
err_blog_no_text: 'Text or title is not specified'
|
||||
err_blog_no_date: 'Date is not specified'
|
||||
err_blog_db_err: 'Database error'
|
||||
err_blog_no_short_name: 'Short name not specified'
|
||||
err_blog_short_name_exists: 'This short name already exists'
|
||||
@ -92,12 +82,12 @@ pages_write_form_toggle_wrap: 'Toggle wrap'
|
||||
pages_write_form_options: 'Options'
|
||||
|
||||
# pages (errors)
|
||||
err_pages_no_title: 'Title not specified'
|
||||
err_pages_no_text: 'Text not specified'
|
||||
err_pages_no_text: 'Text or title is not specified'
|
||||
err_pages_no_id: 'ID not specified'
|
||||
err_pages_no_short_name: 'Short name not specified'
|
||||
err_pages_db_err: 'Database error'
|
||||
|
||||
# admin-switch
|
||||
admin_title: "Admin"
|
||||
as_form_password: 'Password'
|
||||
admin_password: 'Password'
|
||||
admin_login: "Login"
|
||||
|
@ -1,21 +1,89 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
namespace cli_util;
|
||||
require_once __DIR__.'/../init.php';
|
||||
require_once 'lib/admin.php';
|
||||
|
||||
use lib\Config;
|
||||
use lib\Pages;
|
||||
use lib\Posts;
|
||||
use lib\Uploads;
|
||||
use util\cli;
|
||||
(new cli())
|
||||
|
||||
require_once __DIR__.'/init.php';
|
||||
->on('admin-add', function() {
|
||||
list($login, $password) = _get_admin_login_password_input();
|
||||
|
||||
$cli = new cli(__NAMESPACE__);
|
||||
$cli->run();
|
||||
if (admin_exists($login))
|
||||
cli::die("Admin ".$login." already exists");
|
||||
|
||||
function admin_reset(): void {
|
||||
$pwd1 = cli::silentInput("New password: ");
|
||||
$id = admin_add($login, $password);
|
||||
echo "ok: id = $id\n";
|
||||
})
|
||||
|
||||
->on('admin-delete', function() {
|
||||
$login = cli::input('Login: ');
|
||||
if (!admin_exists($login))
|
||||
cli::die("No such admin");
|
||||
if (!admin_delete($login))
|
||||
cli::die("Database error");
|
||||
echo "ok\n";
|
||||
})
|
||||
|
||||
->on('admin-set-password', function() {
|
||||
list($login, $password) = _get_admin_login_password_input();
|
||||
echo admin_set_password($login, $password) ? 'ok' : 'fail';
|
||||
echo "\n";
|
||||
})
|
||||
|
||||
->on('blog-erase', function() {
|
||||
$db = DB();
|
||||
$tables = ['posts', 'posts_texts'];
|
||||
foreach ($tables as $t) {
|
||||
$db->query("TRUNCATE TABLE $t");
|
||||
}
|
||||
})
|
||||
|
||||
->on('posts-html', function() {
|
||||
$kw = ['include_hidden' => true];
|
||||
$posts = posts::getList(0, posts::getCount(...$kw), ...$kw);
|
||||
foreach ($posts as $p) {
|
||||
$texts = $p->getTexts();
|
||||
foreach ($texts as $t) {
|
||||
$t->updateHtml();
|
||||
$t->updateText();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
->on('posts-images', function() {
|
||||
$kw = ['include_hidden' => true];
|
||||
$posts = posts::getList(0, posts::getCount(...$kw), ...$kw);
|
||||
foreach ($posts as $p) {
|
||||
$texts = $p->getTexts();
|
||||
foreach ($texts as $t) {
|
||||
$t->updateImagePreviews(true);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
->on('pages-html', function() {
|
||||
$pages = Pages::getAll();
|
||||
foreach ($pages as $p) {
|
||||
$p->updateHtml();
|
||||
}
|
||||
})
|
||||
|
||||
->on('add-files-to-uploads', function() {
|
||||
$path = cli::input('Enter path: ');
|
||||
if (!file_exists($path))
|
||||
cli::die("file $path doesn't exists");
|
||||
$name = basename($path);
|
||||
$ext = extension($name);
|
||||
$id = Uploads::add($path, $name, '');
|
||||
echo "upload id: $id\n";
|
||||
})
|
||||
|
||||
->run();
|
||||
|
||||
function _get_admin_login_password_input(): array {
|
||||
$login = cli::input('Login: ');
|
||||
$pwd1 = cli::silentInput("Password: ");
|
||||
$pwd2 = cli::silentInput("Again: ");
|
||||
|
||||
if ($pwd1 != $pwd2)
|
||||
@ -24,60 +92,8 @@ function admin_reset(): void {
|
||||
if (trim($pwd1) == '')
|
||||
cli::die("Password can not be empty");
|
||||
|
||||
if (!Config::set('admin_pwd', salt_password($pwd1)))
|
||||
cli::die("Database error");
|
||||
}
|
||||
if (strlen($login) > ADMIN_LOGIN_MAX_LENGTH)
|
||||
cli::die("Login is longer than max length (".ADMIN_LOGIN_MAX_LENGTH.")");
|
||||
|
||||
function admin_check(): void {
|
||||
$pwd = Config::get('admin_pwd');
|
||||
echo is_null($pwd) ? "Not set" : $pwd;
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
function blog_erase(): void {
|
||||
$db = getDb();
|
||||
$tables = ['posts', 'posts_tags', 'tags'];
|
||||
foreach ($tables as $t) {
|
||||
$db->query("TRUNCATE TABLE $t");
|
||||
}
|
||||
}
|
||||
|
||||
function tags_recount(): void {
|
||||
$tags = Posts::getAllTags(true);
|
||||
foreach ($tags as $tag)
|
||||
Posts::recountPostsWithTag($tag->id);
|
||||
}
|
||||
|
||||
function posts_html(): void {
|
||||
$kw = ['include_hidden' => true];
|
||||
$posts = Posts::getPosts(0, Posts::getPostsCount(...$kw), ...$kw);
|
||||
foreach ($posts as $p) {
|
||||
$p->updateHtml();
|
||||
$p->updateText();
|
||||
}
|
||||
}
|
||||
|
||||
function posts_images(): void {
|
||||
$kw = ['include_hidden' => true];
|
||||
$posts = Posts::getPosts(0, Posts::getPostsCount(...$kw), ...$kw);
|
||||
foreach ($posts as $p) {
|
||||
$p->updateImagePreviews(true);
|
||||
}
|
||||
}
|
||||
|
||||
function pages_html(): void {
|
||||
$pages = Pages::getAll();
|
||||
foreach ($pages as $p) {
|
||||
$p->updateHtml();
|
||||
}
|
||||
}
|
||||
|
||||
function add_files_to_uploads(): void {
|
||||
$path = cli::input('Enter path: ');
|
||||
if (!file_exists($path))
|
||||
cli::die("file $path doesn't exists");
|
||||
$name = basename($path);
|
||||
$ext = extension($name);
|
||||
$id = Uploads::add($path, $name, '');
|
||||
echo "upload id: $id\n";
|
||||
return [$login, $pwd1];
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user