321 lines
11 KiB
PHP
321 lines
11 KiB
PHP
<?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 {
|
|
global $AdminSession;
|
|
|
|
$packed = self::pack($action);
|
|
|
|
$data = [
|
|
'action' => $action::getActionId(),
|
|
'ts' => time(),
|
|
];
|
|
|
|
if (is_cli()) {
|
|
$data += [
|
|
'cli' => 1,
|
|
];
|
|
} else {
|
|
$data += [
|
|
'admin_id' => $AdminSession->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]++)] ?? null;
|
|
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 = '\\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;
|
|
}
|
|
|
|
}
|