admin: log actions
This commit is contained in:
parent
68a04ab18d
commit
4a96cc6b5a
@ -100,6 +100,11 @@ function ensure_admin() {
|
||||
forbidden();
|
||||
}
|
||||
|
||||
function ensure_xhr() {
|
||||
if (!is_xhr_request())
|
||||
invalid_request();
|
||||
}
|
||||
|
||||
abstract class request_handler {
|
||||
function __construct() {
|
||||
add_static(
|
||||
|
@ -86,13 +86,16 @@ class AdminHandler extends request_handler {
|
||||
if (!uploads::isExtensionAllowed($ext))
|
||||
redirect('/admin/uploads/?error='.urlencode('extension not allowed'));
|
||||
|
||||
$name = $custom_name ?: $f['name'];
|
||||
$upload_id = uploads::add(
|
||||
$f['tmp_name'],
|
||||
$custom_name ?: $f['name'],
|
||||
$name,
|
||||
$note);
|
||||
|
||||
if (!$upload_id)
|
||||
redirect('/admin/uploads/?error='.urlencode('failed to create upload'));
|
||||
|
||||
admin_log(new \AdminActions\UploadsAdd($upload_id, $name, $note));
|
||||
}
|
||||
|
||||
redirect('/admin/uploads/');
|
||||
@ -105,6 +108,7 @@ class AdminHandler extends request_handler {
|
||||
redirect('/admin/uploads/?error='.urlencode('upload not found'));
|
||||
csrf_check('delupl'.$id);
|
||||
uploads::delete($id);
|
||||
admin_log(new \AdminActions\UploadsDelete($id));
|
||||
redirect('/admin/uploads/');
|
||||
}
|
||||
|
||||
@ -118,12 +122,13 @@ class AdminHandler extends request_handler {
|
||||
csrf_check('editupl'.$id);
|
||||
|
||||
$upload->setNote($note);
|
||||
|
||||
admin_log(new \AdminActions\UploadsEditNote($id, $note));
|
||||
redirect('/admin/uploads/');
|
||||
}
|
||||
|
||||
function POST_ajax_md_preview() {
|
||||
if (!is_xhr_request())
|
||||
forbidden();
|
||||
ensure_xhr();
|
||||
list($md, $title, $use_image_previews) = input('md, title, b:use_image_previews');
|
||||
$html = markup::markdownToHtml($md, $use_image_previews);
|
||||
$ctx = new SkinContext('\\skin\\admin');
|
||||
@ -177,10 +182,95 @@ class AdminHandler extends request_handler {
|
||||
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,
|
||||
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() {
|
||||
add_skin_strings_re('/^(err_)?blog_/');
|
||||
set_title('$blog_write');
|
||||
@ -205,9 +295,7 @@ class AdminHandler extends request_handler {
|
||||
}
|
||||
|
||||
function POST_post_add() {
|
||||
if (!is_xhr_request())
|
||||
invalid_request();
|
||||
|
||||
ensure_xhr();
|
||||
csrf_check('post_add');
|
||||
|
||||
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']);
|
||||
|
||||
// add texts
|
||||
$added_texts = []; // for admin actions logging, at the end
|
||||
foreach ($lang_data as $lang => $data) {
|
||||
list($title, $text, $toc_enabled) = $data;
|
||||
if (!$post->addText(
|
||||
if (!($new_post_text = $post->addText(
|
||||
lang: PostLanguage::from($lang),
|
||||
title: $title,
|
||||
md: $text,
|
||||
toc: $toc_enabled)
|
||||
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()]);
|
||||
}
|
||||
|
||||
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() {
|
||||
list($name) = input('short_name');
|
||||
|
||||
@ -289,8 +365,10 @@ class AdminHandler extends request_handler {
|
||||
if (!$post)
|
||||
not_found();
|
||||
|
||||
csrf_check('delpost'.$post->id);
|
||||
$id = $post->id;
|
||||
csrf_check('delpost'.$id);
|
||||
posts::delete($post);
|
||||
admin_log(new \AdminActions\PostDelete($id));
|
||||
redirect('/articles/', code: HTTPCode::Found);
|
||||
}
|
||||
|
||||
@ -299,7 +377,9 @@ class AdminHandler extends request_handler {
|
||||
$lang = PostLanguage::from($lang);
|
||||
|
||||
$post = posts::getByName($short_name);
|
||||
if ($post) {
|
||||
if (!$post)
|
||||
not_found();
|
||||
|
||||
$texts = $post->getTexts();
|
||||
if (!isset($texts[$lang->value]))
|
||||
not_found();
|
||||
@ -345,12 +425,8 @@ class AdminHandler extends request_handler {
|
||||
);
|
||||
}
|
||||
|
||||
not_found();
|
||||
}
|
||||
|
||||
function POST_post_edit() {
|
||||
if (!is_xhr_request())
|
||||
invalid_request();
|
||||
ensure_xhr();
|
||||
|
||||
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->edit($post_data);
|
||||
|
||||
admin_log(new \AdminActions\PostEdit($post->id));
|
||||
ajax_ok(['url' => $post->getUrl().'edit/?saved=1&lang='.$lang->value]);
|
||||
|
||||
}
|
||||
|
||||
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() {
|
||||
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 _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() {
|
||||
|
5
init.php
5
init.php
@ -22,10 +22,13 @@ spl_autoload_register(function($class) {
|
||||
'engine/skin' => ['SkinContext'],
|
||||
];
|
||||
|
||||
if (str_contains($class, '\\'))
|
||||
$class = str_replace('\\', '/', $class);
|
||||
|
||||
$path = null;
|
||||
foreach (['Handler', 'Helper'] as $sfx) {
|
||||
if (str_ends_with($class, $sfx)) {
|
||||
$path = APP_ROOT.'/'.strtolower($sfx).'/'.str_replace('\\', '/', $class).'.php';
|
||||
$path = APP_ROOT.'/'.strtolower($sfx).'/'.$class.'.php';
|
||||
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_COOKIE_NAME = 'admin_key';
|
||||
const ADMIN_LOGIN_MAX_LENGTH = 32;
|
||||
|
||||
$AdminSession = [
|
||||
'id' => null,
|
||||
'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 {
|
||||
$db = DB();
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
function admin_set_password(string $login, string $password): bool {
|
||||
$db = DB();
|
||||
$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,
|
||||
];
|
||||
|
||||
admin_set_cookie($token);
|
||||
_admin_set_cookie($token);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -135,15 +109,45 @@ function admin_logout() {
|
||||
$AdminSession['login'] = null;
|
||||
$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;
|
||||
setcookie(ADMIN_COOKIE_NAME, $token, time() + ADMIN_SESSION_TIMEOUT, '/', $config['cookie_host']);
|
||||
}
|
||||
|
||||
function admin_unset_cookie(): void {
|
||||
function _admin_unset_cookie(): void {
|
||||
global $config;
|
||||
setcookie(ADMIN_COOKIE_NAME, '', 1, '/', $config['cookie_host']);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user