2024-03-09 16:20:00 +00:00

319 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 {
$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;
}
}