true]); } function before_dispatch(string $http_method, string $action) { if ($action != 'login' && !is_admin()) forbidden(); } function GET_index() { global $AdminSession; //$admin_info = admin_current_info(); set_title('$admin_title'); render('admin/index', admin_login: $AdminSession->login); } function GET_login() { if (is_admin()) redirect('/admin/'); set_title('$admin_title'); render('admin/login'); } function POST_login() { csrf_check('adminlogin'); list($login, $password) = input('login, password'); admin_auth($login, $password) ? redirect('/admin/') : forbidden(); } function GET_logout() { csrf_check('logout'); admin_logout(); redirect('/admin/login/', HTTPCode::Found); } function GET_errors() { list($ip, $query, $url_query, $file_query, $line_query, $per_page) = input('i:ip, query, url_query, file_query, i:line_query, i:per_page'); if (!$per_page) $per_page = 100; $db = DB(); $query = trim($query ?? ''); $url_query = trim($url_query ?? ''); $sql_where = []; if ($ip) { $sql_where[] = "ip='".$db->escape($ip)."'"; } if ($query) { $sql_where[] = "text LIKE '%".$db->escape($query)."%'"; } if ($url_query) { $sql_where[] = "url LIKE '%".$db->escape($url_query)."%'"; } if ($file_query) { $sql_where[] = "file LIKE '%".$db->escape($file_query)."%'"; } if ($line_query) { $sql_where[] = "line='".$db->escape($line_query)."'"; } if (!empty($sql_where)) { $sql_where = " WHERE ".implode(' AND ', $sql_where)." "; } else { $sql_where = ''; } $count = (int)$db->result($db->query("SELECT COUNT(*) FROM backend_errors".$sql_where)); list($page, $pages, $offset) = get_page($per_page, $count); $q = $db->query("SELECT *, INET_NTOA(ip) ip_s FROM backend_errors $sql_where ORDER BY id DESC LIMIT $offset, $per_page"); $list = []; while ($row = $db->fetch($q)) { $row['date'] = format_time($row['ts'], [ 'seconds' => true, 'short_months' => true, ]); $row['full_url'] = !str_starts_with($row['url'], 'https://') ? 'https://'.$row['url'] : $row['url']; $error_name = getPHPErrorName((int)$row['errno']); if (!is_null($error_name)) $row['errtype'] = $error_name; $list[] = $row; } // url for pageNav $url = '/admin/errors/'; $url_params = []; if ($ip) { $url_params['ip'] = $ip; } if ($query) { $url_params['query'] = $query; } if ($url_query) { $url_params['url_query'] = $url_query; } if ($file_query) { $url_params['file_query'] = $file_query; } if ($line_query) { $url_params['line_query'] = $line_query; } if (!empty($url_params)) { $url .= '?'.http_build_query($url_params).'&'; } else { $url .= '?'; } $vars = [ 'list' => $list, 'count' => $count, 'pn_page' => $page, 'pn_pages' => $pages, 'url' => $url, ]; if ($ip) { $vars += [ 'ip_filter' => ulong2ip($ip), 'ip' => $ip ]; } $query_var_names = ['query', 'url_query', 'file_query', 'line_query']; foreach ($query_var_names as $query_var_name) { if ($$query_var_name) { $vars += [$query_var_name => $$query_var_name]; } } set_skin_opts(['wide' => true]); set_title('$admin_errors'); render('admin/errors', ...$vars); } function GET_auth_log() { $db = DB(); $count = (int)$db->result($db->query("SELECT COUNT(*) FROM admin_log")); $per_page = 100; list($page, $pages, $offset) = get_page($per_page, $count); $q = $db->query("SELECT *, INET_NTOA(ip) AS ip, admins.login AS login, admins.activity_ts AS activitiy_ts FROM admin_log LEFT JOIN admins ON admins.id=admin_log.admin_id ORDER BY admin_log.id DESC LIMIT $offset, ".$per_page); $list = $db->fetchAll($q); if (!empty($list)) { $list = array_map(function($item) { $item['date'] = format_time($item['ts']); $item['activity_ts_s'] = format_time($item['activity_ts']); return $item; }, $list); } $vars = [ 'list' => $list, 'pn_page' => $page, 'pn_pages' => $pages ]; set_skin_opts(['wide' => true]); set_title('$admin_auth_log'); render('admin/auth_log', ...$vars); } function GET_actions_log() { $field_types = \AdminActions\Util\Logger::getFieldTypes(); foreach ($field_types as $type_prefix => $type_data) { for ($i = 1; $i <= $type_data['count']; $i++) { $name = $type_prefix.'arg'.$i; if (isset($_REQUEST[$name]) && (string)$_REQUEST[$name] !== '') $argument_filters[$name] = $type_data['unpacker']((string)$_REQUEST[$name]); } } $per_page = 100; $count = \AdminActions\Util\Logger::getRecordsCount(); list($page, $pages, $offset) = get_page($per_page, $count); $admin_ids = []; $admin_logins = []; $records = \AdminActions\Util\Logger::getRecords($offset, $per_page); foreach ($records as $record) { list($admin_id) = $record->getActorInfo(); if ($admin_id !== null) $admin_ids[$admin_id] = true; } if (!empty($admin_ids)) $admin_logins = admin_get_logins_by_id(array_keys($admin_ids)); $url = '/admin/actions-log/?'; $filter_fields = []; foreach ($field_types as $type_prefix => $type_data) { for ($i = 1; $i <= $type_data['count']; $i++) { $name = $type_prefix.'arg'.$i; $filter_fields[$name] = $argument_filters[$name] ?? ''; } } $vars = [ 'list' => $records, 'pn_page' => $page, 'pn_pages' => $pages, 'admin_logins' => $admin_logins, 'url' => $url, 'action_types' => \AdminActions\Util\Logger::getActions(true), ]; set_skin_opts(['wide' => true]); set_title('$admin_actions_log'); render('admin/actions_log', ...$vars); } function GET_uploads() { list($error) = input('error'); $uploads = uploads::getAllUploads(); set_title('$blog_upload'); render('admin/uploads', error: $error, uploads: $uploads, langs: PostLanguage::cases()); } function POST_uploads() { csrf_check('addupl'); list($custom_name, $note_en, $note_ru) = input('name, note_en, note_ru'); if (!isset($_FILES['files'])) redirect('/admin/uploads/?error='.urlencode('no file')); $files = []; for ($i = 0; $i < count($_FILES['files']['name']); $i++) { $files[] = [ 'name' => $_FILES['files']['name'][$i], 'type' => $_FILES['files']['type'][$i], 'tmp_name' => $_FILES['files']['tmp_name'][$i], 'error' => $_FILES['files']['error'][$i], 'size' => $_FILES['files']['size'][$i], ]; } if (count($files) > 1) { $note_en = ''; $note_ru = ''; $custom_name = ''; } foreach ($files as $f) { if ($f['error']) redirect('/admin/uploads/?error='.urlencode('error code '.$f['error'])); if (!$f['size']) redirect('/admin/uploads/?error='.urlencode('received empty file')); $ext = extension($f['name']); if (!uploads::isExtensionAllowed($ext)) redirect('/admin/uploads/?error='.urlencode('extension not allowed')); $name = $custom_name ?: $f['name']; $upload_id = uploads::add( $f['tmp_name'], $name, $note_en, $note_ru); if (!$upload_id) redirect('/admin/uploads/?error='.urlencode('failed to create upload')); admin_log(new \AdminActions\UploadsAdd($upload_id, $name, $note_en, $note_ru)); } redirect('/admin/uploads/'); } function GET_upload_delete() { list($id) = input('i:id'); $upload = uploads::get($id); if (!$upload) redirect('/admin/uploads/?error='.urlencode('upload not found')); csrf_check('delupl'.$id); uploads::delete($id); admin_log(new \AdminActions\UploadsDelete($id)); redirect('/admin/uploads/'); } function POST_upload_edit_note() { list($id, $note, $lang) = input('i:id, note, lang'); $lang = PostLanguage::tryFrom($lang); if (!$lang) not_found(); $upload = uploads::get($id); if (!$upload) redirect('/admin/uploads/?error='.urlencode('upload not found')); csrf_check('editupl'.$id); $upload->setNote($lang, $note); $texts = posts::getTextsWithUpload($upload); if (!empty($texts)) { foreach ($texts as $text) { $text->updateHtml(); $text->updateText(); } } admin_log(new \AdminActions\UploadsEditNote($id, $note, $lang->value)); redirect('/admin/uploads/'); } function POST_ajax_md_preview() { ensure_xhr(); list($md, $title, $use_image_previews, $lang, $is_page) = input('md, title, b:use_image_previews, lang, b:is_page'); $lang = PostLanguage::tryFrom($lang); if (!$lang) $lang = PostLanguage::getDefault(); if ($is_page) { $md = '# '.$title."\n\n".$md; $title = ''; } $html = markup::markdownToHtml($md, $use_image_previews, $lang); $ctx = skin('admin'); $html = $ctx->markdownPreview( unsafe_html: $html, title: $title ); ajax_ok(['html' => $html]); } function GET_page_add() { list($name) = input('short_name'); $page = pages::getByName($name); if ($page) redirect($page->getUrl(), code: HTTPCode::Found); add_skin_strings_re('/^(err_)?pages_/'); add_skin_strings_re('/^(err_)?blog_/'); set_title(lang('pages_create_title', $name)); static::make_wide(); render('admin/pageForm', short_name: $name, title: '', text: '', langs: PostLanguage::cases()); } function POST_page_add() { csrf_check('addpage'); list($name, $text, $title) = input('short_name, text, title'); $page = pages::getByName($name); if ($page) not_found(); $error_code = null; if (!$title) { $error_code = 'no_title'; } else if (!$text) { $error_code = 'no_text'; } if ($error_code) ajax_error(['code' => $error_code]); if (!pages::add([ 'short_name' => $name, 'title' => $title, 'md' => $text ])) { ajax_error(['code' => 'db_err']); } admin_log(new \AdminActions\PageCreate($name)); $page = pages::getByName($name); ajax_ok(['url' => $page->getUrl()]); } function GET_page_delete() { list($name) = input('short_name'); $page = pages::getByName($name); if (!$page) not_found(); $url = $page->getUrl(); csrf_check('delpage'.$page->shortName); pages::delete($page); admin_log(new \AdminActions\PageDelete($name)); redirect($url, code: HTTPCode::Found); } function GET_page_edit() { list($short_name, $saved) = input('short_name, b:saved'); $page = pages::getByName($short_name); if (!$page) not_found(); add_skin_strings_re('/^(err_)?pages_/'); add_skin_strings_re('/^(err_)?blog_/'); set_title(lang('pages_page_edit_title', $page->shortName.'.html')); static::make_wide(); $js_text = [ 'text' => $page->md, 'title' => $page->title, ]; render('admin/pageForm', is_edit: true, short_name: $page->shortName, title: $page->title, text: $page->md, visible: $page->visible, render_title: $page->renderTitle, saved: $saved, langs: PostLanguage::cases(), js_text: $js_text); } function POST_page_edit() { ensure_xhr(); list($short_name) = input('short_name'); $page = pages::getByName($short_name); if (!$page) not_found(); csrf_check('editpage'.$page->shortName); list($text, $title, $visible, $short_name, $render_title) = input('text, title, b:visible, new_short_name, b:render_title'); $text = trim($text); $title = trim($title); $error_code = null; if (!$title) { $error_code = 'no_title'; } else if (!$text) { $error_code = 'no_text'; } else if (!$short_name) { $error_code = 'no_short_name'; } if ($error_code) ajax_error(['code' => $error_code]); $new_short_name = $page->shortName != $short_name ? $short_name : null; previous_texts::add(PreviousText::TYPE_PAGE, $page->get_id(), $page->md, $page->updateTs ?: $page->ts); $page->edit([ 'title' => $title, 'md' => $text, 'visible' => (bool)$visible, 'short_name' => $short_name, 'render_title' => (bool)$render_title ]); admin_log(new \AdminActions\PageEdit($short_name, $new_short_name)); ajax_ok(['url' => $page->getUrl().'edit/?saved=1']); } function GET_post_add() { 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, 'keywords' => '', ]; } render('admin/postForm', title: '', text: '', langs: PostLanguage::cases(), short_name: '', source_url: '', keywords: '', js_texts: $js_texts, lang: PostLanguage::getDefault()->value); } function POST_post_add() { ensure_xhr(); 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, $keywords, $toc_enabled) = input("title:{$lang->value}, text:{$lang->value}, keywords:{$lang->value}, b:toc:{$lang->value}", ['trim' => true]); if ($title !== '' && $text !== '') { $lang_data[$lang->value] = [$title, $text, $keywords, $toc_enabled]; $at_least_one_lang_is_written = true; } } $error_code = null; if (!$at_least_one_lang_is_written) { $error_code = 'no_text'; } else if (empty($short_name)) { $error_code = 'no_short_name'; } if ($error_code) ajax_error(['code' => $error_code]); $post = posts::add([ 'visible' => $visibility_enabled, 'short_name' => $short_name, 'date' => $date, 'source_url' => '' ]); if (!$post) ajax_error(['code' => 'db_err', 'message' => 'failed to add post']); // add texts $added_texts = []; // for admin actions logging, at the end foreach ($lang_data as $lang => $data) { list($title, $text, $keywords, $toc_enabled) = $data; if (!($new_post_text = $post->addText( lang: PostLanguage::from($lang), title: $title, md: $text, keywords: $keywords, toc: $toc_enabled)) ) { posts::delete($post); ajax_error(['code' => 'db_err', 'message' => 'failed to add text language '.$lang]); } else { $added_texts[] = [$new_post_text->id, $lang]; } } admin_log(new \AdminActions\PostCreate($post->id)); foreach ($added_texts as $added_text) { list($id, $lang) = $added_text; admin_log(new \AdminActions\PostTextCreate($id, $post->id, $lang)); } // done ajax_ok(['url' => $post->getUrl()]); } function GET_post_delete() { list($name) = input('short_name'); $post = posts::getByName($name); if (!$post) not_found(); $id = $post->id; csrf_check('delpost'.$id); posts::delete($post); admin_log(new \AdminActions\PostDelete($id)); redirect('/articles/', code: HTTPCode::Found); } 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) not_found(); $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, 'keywords' => $text->keywords, ]; } else { $js_texts[$pl->value] = [ 'title' => '', 'md' => '', 'toc' => false, 'keywords' => '', ]; } } $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, source_url: $post->sourceUrl, keywords: $text->keywords, langs: PostLanguage::cases(), lang: $text->lang->value, js_texts: $js_texts ); } function POST_post_edit() { ensure_xhr(); list($old_short_name, $short_name, $langs, $date, $source_url) = input('short_name, new_short_name, langs, date, source_url'); $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, $keywords) = input("text:{$lang->value}, title:{$lang->value}, b:visible, b:toc:{$lang->value}, keywords:{$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, keywords: $keywords, toc: $toc ); if (!$pt) ajax_error(['code' => 'db_err']); } else { previous_texts::add(PreviousText::TYPE_POST_TEXT, $pt->id, $pt->md, $post->getUpdateTimestamp() ?: $post->getTimestamp()); $pt->edit([ 'title' => $title, 'md' => $text, 'toc' => (int)$toc, 'keywords' => $keywords ]); } } $post_data = [ 'date' => $date, 'visible' => $visible, 'source_url' => $source_url ]; if ($post->shortName != $short_name) $post_data['short_name'] = $short_name; $post->edit($post_data); admin_log(new \AdminActions\PostEdit($post->id)); ajax_ok(['url' => $post->getUrl().'edit/?saved=1&lang='.$lang->value]); } function GET_books() { set_title('$admin_books'); render('admin/books'); } 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']); } protected static function make_wide() { set_skin_opts([ 'full_width' => true, 'no_footer' => true ]); } }