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