admin: log actions
This commit is contained in:
parent
68a04ab18d
commit
4a96cc6b5a
@ -100,6 +100,11 @@ function ensure_admin() {
|
|||||||
forbidden();
|
forbidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensure_xhr() {
|
||||||
|
if (!is_xhr_request())
|
||||||
|
invalid_request();
|
||||||
|
}
|
||||||
|
|
||||||
abstract class request_handler {
|
abstract class request_handler {
|
||||||
function __construct() {
|
function __construct() {
|
||||||
add_static(
|
add_static(
|
||||||
|
@ -86,13 +86,16 @@ class AdminHandler extends request_handler {
|
|||||||
if (!uploads::isExtensionAllowed($ext))
|
if (!uploads::isExtensionAllowed($ext))
|
||||||
redirect('/admin/uploads/?error='.urlencode('extension not allowed'));
|
redirect('/admin/uploads/?error='.urlencode('extension not allowed'));
|
||||||
|
|
||||||
|
$name = $custom_name ?: $f['name'];
|
||||||
$upload_id = uploads::add(
|
$upload_id = uploads::add(
|
||||||
$f['tmp_name'],
|
$f['tmp_name'],
|
||||||
$custom_name ?: $f['name'],
|
$name,
|
||||||
$note);
|
$note);
|
||||||
|
|
||||||
if (!$upload_id)
|
if (!$upload_id)
|
||||||
redirect('/admin/uploads/?error='.urlencode('failed to create upload'));
|
redirect('/admin/uploads/?error='.urlencode('failed to create upload'));
|
||||||
|
|
||||||
|
admin_log(new \AdminActions\UploadsAdd($upload_id, $name, $note));
|
||||||
}
|
}
|
||||||
|
|
||||||
redirect('/admin/uploads/');
|
redirect('/admin/uploads/');
|
||||||
@ -105,6 +108,7 @@ class AdminHandler extends request_handler {
|
|||||||
redirect('/admin/uploads/?error='.urlencode('upload not found'));
|
redirect('/admin/uploads/?error='.urlencode('upload not found'));
|
||||||
csrf_check('delupl'.$id);
|
csrf_check('delupl'.$id);
|
||||||
uploads::delete($id);
|
uploads::delete($id);
|
||||||
|
admin_log(new \AdminActions\UploadsDelete($id));
|
||||||
redirect('/admin/uploads/');
|
redirect('/admin/uploads/');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,12 +122,13 @@ class AdminHandler extends request_handler {
|
|||||||
csrf_check('editupl'.$id);
|
csrf_check('editupl'.$id);
|
||||||
|
|
||||||
$upload->setNote($note);
|
$upload->setNote($note);
|
||||||
|
|
||||||
|
admin_log(new \AdminActions\UploadsEditNote($id, $note));
|
||||||
redirect('/admin/uploads/');
|
redirect('/admin/uploads/');
|
||||||
}
|
}
|
||||||
|
|
||||||
function POST_ajax_md_preview() {
|
function POST_ajax_md_preview() {
|
||||||
if (!is_xhr_request())
|
ensure_xhr();
|
||||||
forbidden();
|
|
||||||
list($md, $title, $use_image_previews) = input('md, title, b:use_image_previews');
|
list($md, $title, $use_image_previews) = input('md, title, b:use_image_previews');
|
||||||
$html = markup::markdownToHtml($md, $use_image_previews);
|
$html = markup::markdownToHtml($md, $use_image_previews);
|
||||||
$ctx = new SkinContext('\\skin\\admin');
|
$ctx = new SkinContext('\\skin\\admin');
|
||||||
@ -177,10 +182,95 @@ class AdminHandler extends request_handler {
|
|||||||
ajax_error(['code' => 'db_err']);
|
ajax_error(['code' => 'db_err']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
admin_log(new \AdminActions\PageCreate($name));
|
||||||
|
|
||||||
$page = pages::getByName($name);
|
$page = pages::getByName($name);
|
||||||
ajax_ok(['url' => $page->getUrl()]);
|
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,
|
||||||
|
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)
|
||||||
|
= input('text, title, b:visible, new_short_name');
|
||||||
|
|
||||||
|
$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;
|
||||||
|
|
||||||
|
$page->edit([
|
||||||
|
'title' => $title,
|
||||||
|
'md' => $text,
|
||||||
|
'visible' => (int)$visible,
|
||||||
|
'short_name' => $short_name,
|
||||||
|
]);
|
||||||
|
|
||||||
|
admin_log(new \AdminActions\PageEdit($short_name, $new_short_name));
|
||||||
|
ajax_ok(['url' => $page->getUrl().'edit/?saved=1']);
|
||||||
|
}
|
||||||
|
|
||||||
function GET_post_add() {
|
function GET_post_add() {
|
||||||
add_skin_strings_re('/^(err_)?blog_/');
|
add_skin_strings_re('/^(err_)?blog_/');
|
||||||
set_title('$blog_write');
|
set_title('$blog_write');
|
||||||
@ -205,9 +295,7 @@ class AdminHandler extends request_handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function POST_post_add() {
|
function POST_post_add() {
|
||||||
if (!is_xhr_request())
|
ensure_xhr();
|
||||||
invalid_request();
|
|
||||||
|
|
||||||
csrf_check('post_add');
|
csrf_check('post_add');
|
||||||
|
|
||||||
list($visibility_enabled, $short_name, $langs, $date)
|
list($visibility_enabled, $short_name, $langs, $date)
|
||||||
@ -244,44 +332,32 @@ class AdminHandler extends request_handler {
|
|||||||
ajax_error(['code' => 'db_err', 'message' => 'failed to add post']);
|
ajax_error(['code' => 'db_err', 'message' => 'failed to add post']);
|
||||||
|
|
||||||
// add texts
|
// add texts
|
||||||
|
$added_texts = []; // for admin actions logging, at the end
|
||||||
foreach ($lang_data as $lang => $data) {
|
foreach ($lang_data as $lang => $data) {
|
||||||
list($title, $text, $toc_enabled) = $data;
|
list($title, $text, $toc_enabled) = $data;
|
||||||
if (!$post->addText(
|
if (!($new_post_text = $post->addText(
|
||||||
lang: PostLanguage::from($lang),
|
lang: PostLanguage::from($lang),
|
||||||
title: $title,
|
title: $title,
|
||||||
md: $text,
|
md: $text,
|
||||||
toc: $toc_enabled)
|
toc: $toc_enabled))
|
||||||
) {
|
) {
|
||||||
posts::delete($post);
|
posts::delete($post);
|
||||||
ajax_error(['code' => 'db_err', 'message' => 'failed to add text language '.$lang]);
|
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
|
// done
|
||||||
ajax_ok(['url' => $post->getUrl()]);
|
ajax_ok(['url' => $post->getUrl()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
$url = $page->getUrl();
|
|
||||||
|
|
||||||
csrf_check('delpage'.$page->shortName);
|
|
||||||
pages::delete($page);
|
|
||||||
redirect($url, code: HTTPCode::Found);
|
|
||||||
}
|
|
||||||
|
|
||||||
function GET_post_delete() {
|
function GET_post_delete() {
|
||||||
list($name) = input('short_name');
|
list($name) = input('short_name');
|
||||||
|
|
||||||
@ -289,8 +365,10 @@ class AdminHandler extends request_handler {
|
|||||||
if (!$post)
|
if (!$post)
|
||||||
not_found();
|
not_found();
|
||||||
|
|
||||||
csrf_check('delpost'.$post->id);
|
$id = $post->id;
|
||||||
|
csrf_check('delpost'.$id);
|
||||||
posts::delete($post);
|
posts::delete($post);
|
||||||
|
admin_log(new \AdminActions\PostDelete($id));
|
||||||
redirect('/articles/', code: HTTPCode::Found);
|
redirect('/articles/', code: HTTPCode::Found);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,7 +377,9 @@ class AdminHandler extends request_handler {
|
|||||||
$lang = PostLanguage::from($lang);
|
$lang = PostLanguage::from($lang);
|
||||||
|
|
||||||
$post = posts::getByName($short_name);
|
$post = posts::getByName($short_name);
|
||||||
if ($post) {
|
if (!$post)
|
||||||
|
not_found();
|
||||||
|
|
||||||
$texts = $post->getTexts();
|
$texts = $post->getTexts();
|
||||||
if (!isset($texts[$lang->value]))
|
if (!isset($texts[$lang->value]))
|
||||||
not_found();
|
not_found();
|
||||||
@ -345,12 +425,8 @@ class AdminHandler extends request_handler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
not_found();
|
|
||||||
}
|
|
||||||
|
|
||||||
function POST_post_edit() {
|
function POST_post_edit() {
|
||||||
if (!is_xhr_request())
|
ensure_xhr();
|
||||||
invalid_request();
|
|
||||||
|
|
||||||
list($old_short_name, $short_name, $langs, $date) = input('short_name, new_short_name, langs, date');
|
list($old_short_name, $short_name, $langs, $date) = input('short_name, new_short_name, langs, date');
|
||||||
|
|
||||||
@ -401,74 +477,16 @@ class AdminHandler extends request_handler {
|
|||||||
$post_data['short_name'] = $short_name;
|
$post_data['short_name'] = $short_name;
|
||||||
$post->edit($post_data);
|
$post->edit($post_data);
|
||||||
|
|
||||||
|
admin_log(new \AdminActions\PostEdit($post->id));
|
||||||
ajax_ok(['url' => $post->getUrl().'edit/?saved=1&lang='.$lang->value]);
|
ajax_ok(['url' => $post->getUrl().'edit/?saved=1&lang='.$lang->value]);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function GET_page_edit() {
|
protected static function _postEditValidateCommonData($date) {
|
||||||
list($short_name, $saved) = input('short_name, b:saved');
|
$dt = DateTime::createFromFormat("Y-m-d", $date);
|
||||||
|
$date_is_valid = $dt && $dt->format("Y-m-d") === $date;
|
||||||
$page = pages::getByName($short_name);
|
if (!$date_is_valid)
|
||||||
if (!$page)
|
ajax_error(['code' => 'no_date']);
|
||||||
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,
|
|
||||||
saved: $saved,
|
|
||||||
langs: PostLanguage::cases(),
|
|
||||||
js_text: $js_text);
|
|
||||||
}
|
|
||||||
|
|
||||||
function POST_page_edit() {
|
|
||||||
if (!is_xhr_request())
|
|
||||||
forbidden();
|
|
||||||
|
|
||||||
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)
|
|
||||||
= input('text, title, b:visible, new_short_name');
|
|
||||||
|
|
||||||
$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]);
|
|
||||||
|
|
||||||
$page->edit([
|
|
||||||
'title' => $title,
|
|
||||||
'md' => $text,
|
|
||||||
'visible' => (int)$visible,
|
|
||||||
'short_name' => $short_name,
|
|
||||||
]);
|
|
||||||
|
|
||||||
ajax_ok(['url' => $page->getUrl().'edit/?saved=1']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function make_wide() {
|
protected static function make_wide() {
|
||||||
|
5
init.php
5
init.php
@ -22,10 +22,13 @@ spl_autoload_register(function($class) {
|
|||||||
'engine/skin' => ['SkinContext'],
|
'engine/skin' => ['SkinContext'],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (str_contains($class, '\\'))
|
||||||
|
$class = str_replace('\\', '/', $class);
|
||||||
|
|
||||||
$path = null;
|
$path = null;
|
||||||
foreach (['Handler', 'Helper'] as $sfx) {
|
foreach (['Handler', 'Helper'] as $sfx) {
|
||||||
if (str_ends_with($class, $sfx)) {
|
if (str_ends_with($class, $sfx)) {
|
||||||
$path = APP_ROOT.'/'.strtolower($sfx).'/'.str_replace('\\', '/', $class).'.php';
|
$path = APP_ROOT.'/'.strtolower($sfx).'/'.$class.'.php';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
91
lib/AdminActions/BaseAction.php
Normal file
91
lib/AdminActions/BaseAction.php
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AdminActions;
|
||||||
|
|
||||||
|
abstract class BaseAction {
|
||||||
|
|
||||||
|
protected int $recordId;
|
||||||
|
protected ?int $adminId;
|
||||||
|
protected int $timeStamp;
|
||||||
|
protected int $ip;
|
||||||
|
protected bool $isCli;
|
||||||
|
|
||||||
|
protected static array $hashActionIds = [];
|
||||||
|
|
||||||
|
public static function getActionId(): int {
|
||||||
|
if (!array_key_exists(static::class, self::$hashActionIds)) {
|
||||||
|
$class = static::class;
|
||||||
|
if (($pos = strrpos($class, '\\')) !== false)
|
||||||
|
$class = substr($class, $pos+1);
|
||||||
|
$hash = hash('adler32', $class, true);
|
||||||
|
self::$hashActionIds[static::class] = unpack('N', $hash)[1];
|
||||||
|
}
|
||||||
|
return self::$hashActionIds[static::class];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getActionName(): string {
|
||||||
|
$cl = get_class($this);
|
||||||
|
if (($pos = strrpos($cl, '\\')) !== false)
|
||||||
|
$cl = substr($cl, $pos+1);
|
||||||
|
return $cl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMetaInformation(int $record_id,
|
||||||
|
?int $admin_id,
|
||||||
|
int $timestamp,
|
||||||
|
int $ip,
|
||||||
|
bool $is_cli) {
|
||||||
|
$this->recordId = $record_id;
|
||||||
|
$this->adminId = $admin_id;
|
||||||
|
$this->timeStamp = $timestamp;
|
||||||
|
$this->ip = $ip;
|
||||||
|
$this->isCli = $is_cli;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getActorInfo(): array {
|
||||||
|
return [$this->adminId, $this->isCli];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAdminId(): ?int {
|
||||||
|
return $this->adminId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isCommandLineAction(): bool {
|
||||||
|
return $this->isCli;
|
||||||
|
}
|
||||||
|
|
||||||
|
//public function getDate(): string {
|
||||||
|
// return formatTime($this->timeStamp, ['short_months' => true]);
|
||||||
|
//}
|
||||||
|
|
||||||
|
public function getTimeStamp(): int {
|
||||||
|
return $this->timeStamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIPv4(): string {
|
||||||
|
return ulong2ip($this->ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRecordId(): int {
|
||||||
|
return $this->recordId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*function renderHtml(): string {
|
||||||
|
$rc = new \ReflectionClass($this);
|
||||||
|
$lines = [];
|
||||||
|
$fields = $rc->getProperties(\ReflectionProperty::IS_PUBLIC);
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
$name = $field->getName();
|
||||||
|
$rfield = new \ReflectionProperty(get_class($this), $name);
|
||||||
|
if ($rfield->getType()->allowsNull() && is_null($this->{$name}))
|
||||||
|
continue;
|
||||||
|
if (is_array($this->{$name})) {
|
||||||
|
$val = htmlescape(jsonEncode($this->{$name}));
|
||||||
|
} else {
|
||||||
|
$val = htmlescape($this->{$name});
|
||||||
|
}
|
||||||
|
$lines[] = $name.'='.$val;
|
||||||
|
}
|
||||||
|
return implode('<br>', $lines);
|
||||||
|
}*/
|
||||||
|
}
|
11
lib/AdminActions/PageCreate.php
Normal file
11
lib/AdminActions/PageCreate.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AdminActions;
|
||||||
|
|
||||||
|
class PageCreate extends BaseAction {
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public string $name
|
||||||
|
) {}
|
||||||
|
|
||||||
|
}
|
11
lib/AdminActions/PageDelete.php
Normal file
11
lib/AdminActions/PageDelete.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AdminActions;
|
||||||
|
|
||||||
|
class PageDelete extends BaseAction {
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public string $name
|
||||||
|
) {}
|
||||||
|
|
||||||
|
}
|
12
lib/AdminActions/PageEdit.php
Normal file
12
lib/AdminActions/PageEdit.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AdminActions;
|
||||||
|
|
||||||
|
class PageEdit extends BaseAction {
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public string $oldName,
|
||||||
|
public ?string $newName
|
||||||
|
) {}
|
||||||
|
|
||||||
|
}
|
11
lib/AdminActions/PostCreate.php
Normal file
11
lib/AdminActions/PostCreate.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AdminActions;
|
||||||
|
|
||||||
|
class PostCreate extends BaseAction {
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public int $id
|
||||||
|
) {}
|
||||||
|
|
||||||
|
}
|
11
lib/AdminActions/PostDelete.php
Normal file
11
lib/AdminActions/PostDelete.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AdminActions;
|
||||||
|
|
||||||
|
class PostDelete extends BaseAction {
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public int $id
|
||||||
|
) {}
|
||||||
|
|
||||||
|
}
|
11
lib/AdminActions/PostEdit.php
Normal file
11
lib/AdminActions/PostEdit.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AdminActions;
|
||||||
|
|
||||||
|
class PostEdit extends BaseAction {
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public int $id
|
||||||
|
) {}
|
||||||
|
|
||||||
|
}
|
13
lib/AdminActions/PostTextCreate.php
Normal file
13
lib/AdminActions/PostTextCreate.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AdminActions;
|
||||||
|
|
||||||
|
class PostTextCreate extends BaseAction {
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public int $id,
|
||||||
|
public int $postId,
|
||||||
|
public string $lang
|
||||||
|
) {}
|
||||||
|
|
||||||
|
}
|
13
lib/AdminActions/UploadsAdd.php
Normal file
13
lib/AdminActions/UploadsAdd.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AdminActions;
|
||||||
|
|
||||||
|
class UploadsAdd extends BaseAction {
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public int $uploadId,
|
||||||
|
public string $name,
|
||||||
|
public string $note
|
||||||
|
) {}
|
||||||
|
|
||||||
|
}
|
11
lib/AdminActions/UploadsDelete.php
Normal file
11
lib/AdminActions/UploadsDelete.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AdminActions;
|
||||||
|
|
||||||
|
class UploadsDelete extends BaseAction {
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public int $uploadId,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
}
|
12
lib/AdminActions/UploadsEditNote.php
Normal file
12
lib/AdminActions/UploadsEditNote.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AdminActions;
|
||||||
|
|
||||||
|
class UploadsEditNote extends BaseAction {
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public int $uploadId,
|
||||||
|
public string $note
|
||||||
|
) {}
|
||||||
|
|
||||||
|
}
|
318
lib/AdminActions/Util/Logger.php
Normal file
318
lib/AdminActions/Util/Logger.php
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AdminActions\util;
|
||||||
|
|
||||||
|
use AdminActions\BaseAction;
|
||||||
|
|
||||||
|
use ReflectionClass;
|
||||||
|
use ReflectionProperty;
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
|
||||||
|
const TABLE = 'admin_actions';
|
||||||
|
|
||||||
|
const INTS_COUNT = 6;
|
||||||
|
const INTS_PREFIX = 'i';
|
||||||
|
|
||||||
|
const VARCHARS_COUNT = 2;
|
||||||
|
const VARCHARS_PREFIX = 'c';
|
||||||
|
|
||||||
|
const SERIALIZED_COUNT = 1;
|
||||||
|
const SERIALIZED_PREFIX = 's';
|
||||||
|
|
||||||
|
protected static ?array $classes = null;
|
||||||
|
|
||||||
|
public static function record(BaseAction $action): int {
|
||||||
|
$packed = self::pack($action);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'action' => $action::getActionId(),
|
||||||
|
'ts' => time(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (is_cli()) {
|
||||||
|
$data += [
|
||||||
|
'cli' => 1,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$data += [
|
||||||
|
'admin_id' => admin_current_info()['id'],
|
||||||
|
'ip' => !empty($_SERVER['REMOTE_ADDR']) ? ip2ulong($_SERVER['REMOTE_ADDR']) : 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($packed as $prefix => $args) {
|
||||||
|
foreach ($args as $i => $arg) {
|
||||||
|
$name = $prefix.'arg'.($i+1);
|
||||||
|
$data[$name] = $arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = DB();
|
||||||
|
$db->insert(self::TABLE, $data);
|
||||||
|
|
||||||
|
return $db->insertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRecordById(int $id): ?BaseAction {
|
||||||
|
$db = DB();
|
||||||
|
$q = $db->query("SELECT * FROM ".self::TABLE." WHERE id=?", $id);
|
||||||
|
if (!$db->numRows($q))
|
||||||
|
return null;
|
||||||
|
return self::unpack($db->fetch($q));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRecordsCount(?array $admin_types = null,
|
||||||
|
?array $actions = null,
|
||||||
|
?array $arguments = null): int {
|
||||||
|
$db = DB();
|
||||||
|
$sql = "SELECT COUNT(*) FROM ".self::TABLE;
|
||||||
|
$where = self::getSQLSelectConditions($admin_types, $actions, $arguments);
|
||||||
|
if ($where != '')
|
||||||
|
$sql .= " WHERE ".$where;
|
||||||
|
$q = $db->query($sql);
|
||||||
|
return (int)$db->result($q);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $offset
|
||||||
|
* @param int $count
|
||||||
|
* @param array|null $admin_types
|
||||||
|
* @param array|null $actions
|
||||||
|
* @param array|null $arguments
|
||||||
|
* @return BaseAction[]
|
||||||
|
*/
|
||||||
|
public static function getRecords(int $offset,
|
||||||
|
int $count,
|
||||||
|
?array $admin_types = null,
|
||||||
|
?array $actions = null,
|
||||||
|
?array $arguments = null): array {
|
||||||
|
$db = DB();
|
||||||
|
$sql = "SELECT * FROM ".self::TABLE;
|
||||||
|
$where = self::getSQLSelectConditions($admin_types, $actions, $arguments);
|
||||||
|
if ($where != '')
|
||||||
|
$sql .= " WHERE ".$where;
|
||||||
|
$sql .= " ORDER BY ts DESC";
|
||||||
|
$sql .= " LIMIT $offset, $count";
|
||||||
|
return array_map(self::class.'::unpack', $db->fetchAll($db->query($sql)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $user_id
|
||||||
|
* @param int|null $time_from
|
||||||
|
* @param int|null $time_to
|
||||||
|
* @return BaseAction[]
|
||||||
|
*/
|
||||||
|
public static function getUserRecords(int $user_id, ?int $time_from, ?int $time_to): array {
|
||||||
|
$db = DB();
|
||||||
|
$sql = "SELECT * FROM ".self::TABLE." WHERE admin_id={$user_id}";
|
||||||
|
if ($time_from && $time_to)
|
||||||
|
$sql .= " AND ts BETWEEN {$time_from} AND {$time_to} ";
|
||||||
|
$sql .= "ORDER BY ts";
|
||||||
|
return array_map(self::class.'::unpack', $db->fetchAll($db->query($sql)));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function getSQLSelectConditions(?array $admin_types = null,
|
||||||
|
?array $actions = null,
|
||||||
|
?array $arguments = null): string {
|
||||||
|
$wheres = [];
|
||||||
|
$db = DB();
|
||||||
|
|
||||||
|
if (!empty($admin_types))
|
||||||
|
$wheres[] = "admin_type IN ('".implode("', '", $admin_types)."')";
|
||||||
|
|
||||||
|
if (!empty($actions)) {
|
||||||
|
$actions = array_map(
|
||||||
|
/** @var BaseAction|int $action */
|
||||||
|
fn($action) => is_string($action) ? $action::getActionId() : $action, $actions);
|
||||||
|
$wheres[] = "action IN (".implode(',', $actions).")";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($arguments)) {
|
||||||
|
foreach ($arguments as $k => $v) {
|
||||||
|
if (!str_starts_with($k, 'iarg') && !str_starts_with($k, 'carg') && !str_starts_with($k, 'sarg')) {
|
||||||
|
logError(__METHOD__.': filter argument '.$k.' not supported');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$wheres[] = $v === null ? $k."=NULL" : $k."='".$db->escape($v)."'";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return !empty($wheres) ? "(".implode(") AND (", $wheres).")" : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function pack(BaseAction $action): array {
|
||||||
|
$field_types = self::getFieldTypes();
|
||||||
|
$packed = [];
|
||||||
|
$cl = get_class($action);
|
||||||
|
$rc = new ReflectionClass($cl);
|
||||||
|
$fields = $rc->getProperties(ReflectionProperty::IS_PUBLIC);
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
$field_name = $field->getName();
|
||||||
|
$refl_field = new ReflectionProperty($cl, $field_name);
|
||||||
|
$refl_type = $refl_field->getType();
|
||||||
|
if (!$refl_type->isBuiltin()) {
|
||||||
|
logError(__METHOD__.': field "'.$field_name.'" is not of built-in type');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($refl_type->getName()) {
|
||||||
|
case 'int':
|
||||||
|
case 'bool':
|
||||||
|
$prefix = 'i';
|
||||||
|
break;
|
||||||
|
case 'array':
|
||||||
|
$prefix = 's';
|
||||||
|
break;
|
||||||
|
case 'string':
|
||||||
|
$prefix = 'c';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logError(__METHOD__.': unexpected field type: '.$refl_type->getName());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($packed[$prefix]) || count($packed[$prefix]) < $field_types[$prefix]['count']) {
|
||||||
|
$packed[$prefix][] = $field_types[$prefix]['packer']($action->{$field_name});
|
||||||
|
} else {
|
||||||
|
logError(__METHOD__.': max ['.$prefix.'] count ('.$field_types[$prefix]['count'].') exceeded');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $packed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function unpack(array $data): BaseAction {
|
||||||
|
$action_id = (int)$data['action'];
|
||||||
|
$cl = self::getClassByActionId($action_id);
|
||||||
|
|
||||||
|
$field_types = self::getFieldTypes();
|
||||||
|
$counter = [];
|
||||||
|
foreach ($field_types as $type => $tmp)
|
||||||
|
$counter[$type] = 1;
|
||||||
|
|
||||||
|
$rc = new ReflectionClass($cl);
|
||||||
|
$arguments = [];
|
||||||
|
$fields = $rc->getProperties(ReflectionProperty::IS_PUBLIC);
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
$name = $field->getName();
|
||||||
|
$refl_field = new ReflectionProperty($cl, $name);
|
||||||
|
$refl_type = $refl_field->getType();
|
||||||
|
if (!$refl_type->isBuiltin()) {
|
||||||
|
logError(__METHOD__.': field "'.$name.'" is not of built-in type');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($refl_type->getName()) {
|
||||||
|
case 'int':
|
||||||
|
case 'bool':
|
||||||
|
$prefix = 'i';
|
||||||
|
break;
|
||||||
|
case 'array':
|
||||||
|
$prefix = 's';
|
||||||
|
break;
|
||||||
|
case 'string':
|
||||||
|
$prefix = 'c';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logError(__METHOD__.': unexpected field type: '.$refl_type->getName());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$val = $data[$prefix.'arg'.($counter[$prefix]++)];
|
||||||
|
if (!$refl_type->allowsNull() || !is_null($val))
|
||||||
|
$val = $field_types[$prefix]['unpacker']($val);
|
||||||
|
|
||||||
|
$arguments[] = $val;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var BaseAction $obj */
|
||||||
|
try {
|
||||||
|
$obj = new $cl(...$arguments);
|
||||||
|
} catch (\TypeError $e) {
|
||||||
|
logDebug($arguments);
|
||||||
|
logError($e);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
$obj->setMetaInformation((int)$data['id'], (int)$data['admin_id'], (int)$data['ts'], (int)$data['ip'], (bool)$data['cli']);
|
||||||
|
|
||||||
|
return $obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getActions(bool $only_names = false): array {
|
||||||
|
if (is_null(self::$classes)) {
|
||||||
|
$objects = [];
|
||||||
|
$dir = realpath(__DIR__.'/../');
|
||||||
|
$files = scandir($dir);
|
||||||
|
foreach ($files as $f) {
|
||||||
|
// skip non-files
|
||||||
|
if ($f == '.' || $f == '..')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$class_name = substr($f, 0, strpos($f, '.'));
|
||||||
|
$class = '\\knigavuhe\\AdminActions\\'.$class_name;
|
||||||
|
|
||||||
|
if (interface_exists($class) || !class_exists($class)) {
|
||||||
|
// logError(__METHOD__.': class '.$class.' not found');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parents = class_parents($class);
|
||||||
|
$found = false;
|
||||||
|
foreach ($parents as $p) {
|
||||||
|
if (str_ends_with($p, 'BaseAction')) {
|
||||||
|
$found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$found) {
|
||||||
|
// logError(__METHOD__.': parent BaseAction not found in class '.$class);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$objects[$class::getActionId()] = $class;
|
||||||
|
}
|
||||||
|
self::$classes = $objects;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$only_names)
|
||||||
|
return self::$classes;
|
||||||
|
|
||||||
|
return array_map(function(string $cl): string {
|
||||||
|
if (($pos = strrpos($cl, '\\')) !== false)
|
||||||
|
$cl = substr($cl, $pos+1);
|
||||||
|
return $cl;
|
||||||
|
}, self::$classes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getClassByActionId(int $action_id): string {
|
||||||
|
return self::getActions()[$action_id];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getFieldTypes(): array {
|
||||||
|
static $types = [];
|
||||||
|
if (!empty($types))
|
||||||
|
return $types;
|
||||||
|
|
||||||
|
foreach (['INTS', 'VARCHARS', 'SERIALIZED'] as $name) {
|
||||||
|
$prefix = constant("self::{$name}_PREFIX");
|
||||||
|
$count = constant("self::{$name}_COUNT");
|
||||||
|
$types[$prefix] = ['count' => $count];
|
||||||
|
switch ($prefix) {
|
||||||
|
case 'i':
|
||||||
|
$types[$prefix]['unpacker'] = fn($v) => $v === null ? null : intval($v);
|
||||||
|
$types[$prefix]['packer'] = fn($v) => strval($v);
|
||||||
|
break;
|
||||||
|
case 'c':
|
||||||
|
$types[$prefix]['unpacker'] = fn($v) => $v === null ? null : strval($v);
|
||||||
|
$types[$prefix]['packer'] = $types[$prefix]['unpacker'];
|
||||||
|
break;
|
||||||
|
case 's':
|
||||||
|
$types[$prefix]['unpacker'] = fn($v) => $v === null ? null : unserialize($v);
|
||||||
|
$types[$prefix]['packer'] = fn($v) => serialize($v);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $types;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -3,6 +3,7 @@
|
|||||||
const ADMIN_SESSION_TIMEOUT = 86400 * 14;
|
const ADMIN_SESSION_TIMEOUT = 86400 * 14;
|
||||||
const ADMIN_COOKIE_NAME = 'admin_key';
|
const ADMIN_COOKIE_NAME = 'admin_key';
|
||||||
const ADMIN_LOGIN_MAX_LENGTH = 32;
|
const ADMIN_LOGIN_MAX_LENGTH = 32;
|
||||||
|
|
||||||
$AdminSession = [
|
$AdminSession = [
|
||||||
'id' => null,
|
'id' => null,
|
||||||
'auth_id' => 0,
|
'auth_id' => 0,
|
||||||
@ -24,32 +25,6 @@ function admin_current_info(): array {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
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_exists(string $login): bool {
|
function admin_exists(string $login): bool {
|
||||||
$db = DB();
|
$db = DB();
|
||||||
return (int)$db->result($db->query("SELECT COUNT(*) FROM admins WHERE login=? LIMIT 1", $login)) > 0;
|
return (int)$db->result($db->query("SELECT COUNT(*) FROM admins WHERE login=? LIMIT 1", $login)) > 0;
|
||||||
@ -78,7 +53,6 @@ function admin_get_id_by_login(string $login): ?int {
|
|||||||
return $db->numRows($q) > 0 ? (int)$db->result($q) : null;
|
return $db->numRows($q) > 0 ? (int)$db->result($q) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function admin_set_password(string $login, string $password): bool {
|
function admin_set_password(string $login, string $password): bool {
|
||||||
$db = DB();
|
$db = DB();
|
||||||
$db->query("UPDATE admins SET password=? WHERE login=?", salt_password($password), $login);
|
$db->query("UPDATE admins SET password=? WHERE login=?", salt_password($password), $login);
|
||||||
@ -118,7 +92,7 @@ function admin_auth(string $login, string $password): bool {
|
|||||||
'login' => $login,
|
'login' => $login,
|
||||||
];
|
];
|
||||||
|
|
||||||
admin_set_cookie($token);
|
_admin_set_cookie($token);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,15 +109,45 @@ function admin_logout() {
|
|||||||
$AdminSession['login'] = null;
|
$AdminSession['login'] = null;
|
||||||
$AdminSession['auth_id'] = 0;
|
$AdminSession['auth_id'] = 0;
|
||||||
|
|
||||||
admin_unset_cookie();
|
_admin_unset_cookie();
|
||||||
}
|
}
|
||||||
|
|
||||||
function admin_set_cookie(string $token): void {
|
function admin_log(\AdminActions\BaseAction $action) {
|
||||||
|
\AdminActions\Util\Logger::record($action);
|
||||||
|
}
|
||||||
|
|
||||||
|
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_set_cookie(string $token): void {
|
||||||
global $config;
|
global $config;
|
||||||
setcookie(ADMIN_COOKIE_NAME, $token, time() + ADMIN_SESSION_TIMEOUT, '/', $config['cookie_host']);
|
setcookie(ADMIN_COOKIE_NAME, $token, time() + ADMIN_SESSION_TIMEOUT, '/', $config['cookie_host']);
|
||||||
}
|
}
|
||||||
|
|
||||||
function admin_unset_cookie(): void {
|
function _admin_unset_cookie(): void {
|
||||||
global $config;
|
global $config;
|
||||||
setcookie(ADMIN_COOKIE_NAME, '', 1, '/', $config['cookie_host']);
|
setcookie(ADMIN_COOKIE_NAME, '', 1, '/', $config['cookie_host']);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user