mirror of
https://github.com/openvk/chandler.git
synced 2024-11-14 19:19:11 +03:00
Add logs support to DBEntity (#14)
* DBEntity.php with logs * Fix * Update DBEntity.php * Fix 2.0 * Update Log.php * Update Log.php * Update Log.php
This commit is contained in:
parent
73eeaa8b3c
commit
011a88d3ce
7 changed files with 384 additions and 22 deletions
|
@ -7,6 +7,9 @@ chandler:
|
|||
appendExtension: "xhtml"
|
||||
adminUrl: "/chandlerd"
|
||||
exposeChandler: true
|
||||
logs:
|
||||
enabled: true
|
||||
entitiesNamespace: "openvk\\Web\\Models\\Entities\\"
|
||||
|
||||
extensions:
|
||||
path: null
|
||||
|
|
|
@ -141,6 +141,7 @@ class Bootstrap
|
|||
$this->registerDebugger();
|
||||
$this->igniteExtensions();
|
||||
|
||||
\Chandler\Database\CurrentUser::get($_SERVER["REMOTE_ADDR"], $_SERVER["HTTP_USER_AGENT"]);
|
||||
if(!$headless) {
|
||||
header("Referrer-Policy: strict-origin-when-cross-origin");
|
||||
$this->defineIP();
|
||||
|
|
40
chandler/Database/CurrentUser.php
Normal file
40
chandler/Database/CurrentUser.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Chandler\Database;
|
||||
|
||||
class CurrentUser
|
||||
{
|
||||
private static $instance = null;
|
||||
private $ip;
|
||||
private $useragent;
|
||||
|
||||
public function __construct(?string $ip = NULL, ?string $useragent = NULL)
|
||||
{
|
||||
if ($ip)
|
||||
$this->ip = $ip;
|
||||
|
||||
if ($useragent)
|
||||
$this->useragent = $useragent;
|
||||
}
|
||||
|
||||
public static function get($ip, $useragent)
|
||||
{
|
||||
if (self::$instance === null) self::$instance = new self($ip, $useragent);
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function getIP(): string
|
||||
{
|
||||
return $this->ip;
|
||||
}
|
||||
|
||||
public function getUserAgent(): string
|
||||
{
|
||||
return $this->useragent;
|
||||
}
|
||||
|
||||
public static function i()
|
||||
{
|
||||
return self::$instance;
|
||||
}
|
||||
}
|
|
@ -1,9 +1,14 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Chandler\Database;
|
||||
|
||||
use Chandler\Database\DatabaseConnection;
|
||||
use Chandler\Security\Authenticator;
|
||||
use Chandler\Security\User;
|
||||
use Nette\Database\Table\Selection;
|
||||
use Nette\Database\Table\ActiveRow;
|
||||
use Nette\InvalidStateException as ISE;
|
||||
use Chandler\Database\Logs;
|
||||
|
||||
|
||||
abstract class DBEntity
|
||||
|
@ -11,23 +16,25 @@ abstract class DBEntity
|
|||
protected $record;
|
||||
protected $changes;
|
||||
protected $deleted;
|
||||
|
||||
private $user;
|
||||
|
||||
protected $tableName;
|
||||
|
||||
function __construct(?ActiveRow $row = NULL)
|
||||
{
|
||||
if(is_null($row)) return;
|
||||
|
||||
if (is_null($row)) return;
|
||||
|
||||
$_table = $row->getTable()->getName();
|
||||
if($_table !== $this->tableName)
|
||||
if ($_table !== $this->tableName)
|
||||
throw new ISE("Invalid data supplied for model: table $_table is not compatible with table" . $this->tableName);
|
||||
|
||||
|
||||
$this->record = $row;
|
||||
$this->user = Authenticator::i()->getUser();
|
||||
}
|
||||
|
||||
function __call(string $fName, array $args)
|
||||
{
|
||||
if(substr($fName, 0, 3) === "set") {
|
||||
if (substr($fName, 0, 3) === "set") {
|
||||
$field = mb_strtolower(substr($fName, 3));
|
||||
$this->stateChanges($field, $args[0]);
|
||||
} else {
|
||||
|
@ -47,7 +54,7 @@ abstract class DBEntity
|
|||
|
||||
protected function stateChanges(string $column, $value): void
|
||||
{
|
||||
if(!is_null($this->record))
|
||||
if (!is_null($this->record))
|
||||
$t = $this->record->{$column}; #Test if column exists
|
||||
|
||||
$this->changes[$column] = $value;
|
||||
|
@ -60,20 +67,22 @@ abstract class DBEntity
|
|||
|
||||
function isDeleted(): bool
|
||||
{
|
||||
return (bool) $this->getRecord()->deleted;
|
||||
return (bool)$this->getRecord()->deleted;
|
||||
}
|
||||
|
||||
function unwrap(): object
|
||||
{
|
||||
return (object) $this->getRecord()->toArray();
|
||||
return (object)$this->getRecord()->toArray();
|
||||
}
|
||||
|
||||
function delete(bool $softly = true): void
|
||||
{
|
||||
if(is_null($this->record))
|
||||
if (is_null($this->record))
|
||||
throw new ISE("Can't delete a model, that hasn't been flushed to DB. Have you forgotten to call save() first?");
|
||||
|
||||
if($softly) {
|
||||
|
||||
(new Logs)->create($this->user->getId(), $this->getTable()->getName(), get_class($this), 2, $this->record->toArray(), $this->changes);
|
||||
|
||||
if ($softly) {
|
||||
$this->record = $this->getTable()->where("id", $this->record->id)->update(["deleted" => true]);
|
||||
} else {
|
||||
$this->record->delete();
|
||||
|
@ -83,25 +92,46 @@ abstract class DBEntity
|
|||
|
||||
function undelete(): void
|
||||
{
|
||||
if(is_null($this->record))
|
||||
if (is_null($this->record))
|
||||
throw new ISE("Can't undelete a model, that hasn't been flushed to DB. Have you forgotten to call save() first?");
|
||||
|
||||
|
||||
(new Logs)->create($this->user->getId(), $this->getTable()->getName(), get_class($this), 3, $this->record->toArray(), ["deleted" => false]);
|
||||
|
||||
$this->getTable()->where("id", $this->record->id)->update(["deleted" => false]);
|
||||
}
|
||||
|
||||
function save(): void
|
||||
|
||||
function save(?bool $log = true): void
|
||||
{
|
||||
if(is_null($this->record)) {
|
||||
if ($log) {
|
||||
$user_id = Authenticator::i()->getUser()->getId();
|
||||
}
|
||||
|
||||
if (is_null($this->record)) {
|
||||
$this->record = $this->getTable()->insert($this->changes);
|
||||
} else if($this->deleted) {
|
||||
$this->record = $this->getTable()->insert((array) $this->record);
|
||||
|
||||
if ($log && $this->getTable()->getName() !== "ChandlerLogs" && CHANDLER_ROOT_CONF["preferences"]["logs"]["enabled"]) {
|
||||
(new Logs)->create($user_id, $this->getTable()->getName(), get_class($this), 0, $this->record->toArray(), $this->changes);
|
||||
}
|
||||
} else {
|
||||
$this->getTable()->get($this->record->id)->update($this->changes);
|
||||
$this->record = $this->getTable()->get($this->record->id);
|
||||
if ($log && $this->getTable()->getName() !== "ChandlerLogs" && CHANDLER_ROOT_CONF["preferences"]["logs"]["enabled"]) {
|
||||
(new Logs)->create($user_id, $this->getTable()->getName(), get_class($this), 1, $this->record->toArray(), $this->changes);
|
||||
}
|
||||
|
||||
if ($this->deleted) {
|
||||
$this->record = $this->getTable()->insert((array)$this->record);
|
||||
} else {
|
||||
$this->getTable()->get($this->record->id)->update($this->changes);
|
||||
$this->record = $this->getTable()->get($this->record->id);
|
||||
}
|
||||
}
|
||||
|
||||
$this->changes = [];
|
||||
}
|
||||
|
||||
|
||||
function getTableName(): string
|
||||
{
|
||||
return $this->getTable()->getName();
|
||||
}
|
||||
|
||||
use \Nette\SmartObject;
|
||||
}
|
||||
|
|
184
chandler/Database/Log.php
Normal file
184
chandler/Database/Log.php
Normal file
|
@ -0,0 +1,184 @@
|
|||
<?php declare(strict_types=1);
|
||||
namespace Chandler\Database;
|
||||
use Chandler\Database\DatabaseConnection;
|
||||
use Chandler\Security\User;
|
||||
|
||||
class Log extends DBEntity
|
||||
{
|
||||
protected $tableName = "ChandlerLogs";
|
||||
|
||||
function getId(): int
|
||||
{
|
||||
return (int) $this->getRecord()->id;
|
||||
}
|
||||
|
||||
function getUser(): string
|
||||
{
|
||||
return $this->getRecord()->user;
|
||||
}
|
||||
|
||||
function getObjectTable(): string
|
||||
{
|
||||
return $this->getRecord()->object_table;
|
||||
}
|
||||
|
||||
function getObjectId(): int
|
||||
{
|
||||
return $this->getRecord()->object_id;
|
||||
}
|
||||
|
||||
function getObject()
|
||||
{
|
||||
$model = $this->getRecord()->object_model;
|
||||
return new $model(DatabaseConnection::i()->getContext()->table($this->getObjectTable())->get($this->getObjectId()));
|
||||
}
|
||||
|
||||
function getTypeRaw(): int
|
||||
{
|
||||
return $this->getRecord()->type;
|
||||
}
|
||||
|
||||
function getType(): string
|
||||
{
|
||||
return ["logs_added", "logs_edited", "logs_removed", "logs_restored"][$this->getTypeRaw()];
|
||||
}
|
||||
|
||||
function getTypeNom(): string
|
||||
{
|
||||
return ["logs_adding", "logs_editing", "logs_removing", "logs_restoring"][$this->getTypeRaw()];
|
||||
}
|
||||
|
||||
function getObjectType(): string
|
||||
{
|
||||
$type = tr("log_" . $this->getObjectTable());
|
||||
if ($type === "@log_" . $this->getObjectTable()) {
|
||||
return str_replace(CHANDLER_ROOT_CONF["preferences"]["logs"]["entitiesNamespace"], "", $this->getRecord()->object_model);
|
||||
} else {
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
|
||||
function getObjectName(): string
|
||||
{
|
||||
$object = $this->getObject();
|
||||
if (method_exists($object, 'getCanonicalName'))
|
||||
return $object->getCanonicalName();
|
||||
else return "[#" . $this->getObjectId() . "] " . $this->getObjectType();
|
||||
}
|
||||
|
||||
function getLogsText(): string
|
||||
{
|
||||
return $this->getRecord()->logs_text;
|
||||
}
|
||||
|
||||
function getObjectURL(): string
|
||||
{
|
||||
$object = $this->getObject();
|
||||
if (method_exists($object, "getURL") && $this->getObjectTable() !== "videos")
|
||||
return $this->getObject()->getURL();
|
||||
else
|
||||
return "#";
|
||||
}
|
||||
|
||||
function getObjectAvatar(): ?string
|
||||
{
|
||||
$object = $this->getObject();
|
||||
if (method_exists($object, 'getAvatarURL'))
|
||||
return $object->getAvatarURL("normal");
|
||||
else return NULL;
|
||||
}
|
||||
|
||||
function getOldValue(): ?array
|
||||
{
|
||||
return (array) json_decode($this->getRecord()->xdiff_old, true, JSON_UNESCAPED_UNICODE) ?? null;
|
||||
}
|
||||
|
||||
function getNewValue(): ?array
|
||||
{
|
||||
return (array) json_decode($this->getRecord()->xdiff_new, true, JSON_UNESCAPED_UNICODE) ?? null;
|
||||
}
|
||||
|
||||
function getTime(): int
|
||||
{
|
||||
return $this->getRecord()->ts;
|
||||
}
|
||||
|
||||
function diff($old, $new): array
|
||||
{
|
||||
$matrix = array();
|
||||
$maxlen = 0;
|
||||
foreach ($old as $oindex => $ovalue) {
|
||||
$nkeys = array_keys($new, $ovalue);
|
||||
foreach ($nkeys as $nindex) {
|
||||
$matrix[$oindex][$nindex] = isset($matrix[$oindex - 1][$nindex - 1]) ?
|
||||
$matrix[$oindex - 1][$nindex - 1] + 1 : 1;
|
||||
if ($matrix[$oindex][$nindex] > $maxlen) {
|
||||
$maxlen = $matrix[$oindex][$nindex];
|
||||
$omax = $oindex + 1 - $maxlen;
|
||||
$nmax = $nindex + 1 - $maxlen;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($maxlen == 0) return array(array('d' => $old, 'i' => $new));
|
||||
return array_merge(
|
||||
$this->diff(array_slice($old, 0, $omax), array_slice($new, 0, $nmax)),
|
||||
array_slice($new, $nmax, $maxlen),
|
||||
$this->diff(array_slice($old, $omax + $maxlen), array_slice($new, $nmax + $maxlen)));
|
||||
}
|
||||
|
||||
function htmlDiff($old, $new): string
|
||||
{
|
||||
$ret = '';
|
||||
$diff = $this->diff(preg_split("/[\s]+/", $old), preg_split("/[\s]+/", $new));
|
||||
foreach ($diff as $k) {
|
||||
if (is_array($k))
|
||||
$ret .= (!empty($k['d']) ? "<del>" . implode(' ', $k['d']) . "</del> " : '') .
|
||||
(!empty($k['i']) ? "<ins>" . implode(' ', $k['i']) . "</ins> " : '');
|
||||
else $ret .= $k . ' ';
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
function getChanges(): array
|
||||
{
|
||||
$result = $this->getOldValue();
|
||||
$_changes = [];
|
||||
|
||||
if ($this->getTypeRaw() === 1) { // edit
|
||||
$changes = $this->getNewValue();
|
||||
|
||||
foreach ($changes as $field => $value) {
|
||||
$new_value = xdiff_string_patch((string) $result[$field], (string) $value);
|
||||
$diff = preg_replace_callback("/<((?!\/?(?:del|ins)\b)[^>]*)>/i", function ($matches) {
|
||||
return '[' . str_replace(['<', '>'], ['[', ']'], $matches[1]) . ']';
|
||||
}, $this->htmlDiff((string)$result[$field], (string)$new_value));
|
||||
|
||||
$_changes[$field] = [
|
||||
"field" => $field,
|
||||
"old_value" => $result[$field],
|
||||
"new_value" => strlen($new_value) > 0 ? $new_value : "(empty)",
|
||||
"ts" => $this->getTime(),
|
||||
"diff" => $diff
|
||||
];
|
||||
}
|
||||
} else if ($this->getTypeRaw() === 0) { // create
|
||||
foreach ($result as $field => $value) {
|
||||
$_changes[$field] = [
|
||||
"field" => $field,
|
||||
"old_value" => $value,
|
||||
"ts" => $this->getTime()
|
||||
];
|
||||
}
|
||||
} else if ($this->getTypeRaw() === 2) { // delete
|
||||
$_changes[] = [
|
||||
"field" => "deleted",
|
||||
"old_value" => 0,
|
||||
"new_value" => 1,
|
||||
"ts" => $this->getTime(),
|
||||
"diff" => $this->htmlDiff("0", "1")
|
||||
];
|
||||
}
|
||||
|
||||
return $_changes;
|
||||
}
|
||||
}
|
86
chandler/Database/Logs.php
Normal file
86
chandler/Database/Logs.php
Normal file
|
@ -0,0 +1,86 @@
|
|||
<?php declare(strict_types=1);
|
||||
namespace Chandler\Database;
|
||||
use Chandler\Database\DatabaseConnection;
|
||||
use Nette\Database\Table\ActiveRow;
|
||||
use Chandler\Database\Log;
|
||||
use Chandler\Database\CurrentUser;
|
||||
|
||||
class Logs
|
||||
{
|
||||
private $context;
|
||||
private $logs;
|
||||
|
||||
function __construct()
|
||||
{
|
||||
$this->context = DatabaseConnection::i()->getContext();
|
||||
$this->logs = $this->context->table("ChandlerLogs");
|
||||
}
|
||||
|
||||
private function toLog(?ActiveRow $ar): ?Log
|
||||
{
|
||||
return is_null($ar) ? NULL : new Log($ar);
|
||||
}
|
||||
|
||||
function get(int $id): ?Log
|
||||
{
|
||||
return $this->toLog($this->logs->get($id));
|
||||
}
|
||||
|
||||
function create(string $user, string $table, string $model, int $type, $object, $changes, ?string $ip = NULL, ?string $useragent = NULL): void
|
||||
{
|
||||
if ($model !== "Chandler\Database\Log") {
|
||||
$fobject = (is_array($object) ? $object : $object->unwrap());
|
||||
$nobject = [];
|
||||
$_changes = [];
|
||||
|
||||
if ($type === 1) {
|
||||
foreach ($changes as $field => $value) {
|
||||
$nobject[$field] = $fobject[$field];
|
||||
}
|
||||
|
||||
foreach (array_diff_assoc($nobject, $changes) as $field => $value) {
|
||||
if (str_starts_with($field, "rate_limit")) continue;
|
||||
if ($field === "online") continue;
|
||||
$_changes[$field] = xdiff_string_diff((string)$nobject[$field], (string)$changes[$field]);
|
||||
}
|
||||
|
||||
if (count($_changes) === 0) return;
|
||||
} else if ($type === 0) { // if new
|
||||
$nobject = $fobject;
|
||||
foreach ($fobject as $field => $value) {
|
||||
$_changes[$field] = xdiff_string_diff("", (string)$value);
|
||||
}
|
||||
} else if ($type === 2 || $type === 3) { // if deleting or restoring
|
||||
$_changes["deleted"] = (int)($type === 2);
|
||||
}
|
||||
|
||||
$log = new Log;
|
||||
$log->setUser($user);
|
||||
$log->setType($type);
|
||||
$log->setObject_Table($table);
|
||||
$log->setObject_Model($model);
|
||||
$log->setObject_Id(is_array($object) ? $object["id"] : $object->getId());
|
||||
$log->setXdiff_Old(json_encode($nobject));
|
||||
$log->setXdiff_New(json_encode($_changes));
|
||||
$log->setTs(time());
|
||||
$log->setIp(CurrentUser::i()->getIP());
|
||||
$log->setUserAgent(CurrentUser::i()->getUserAgent());
|
||||
$log->save();
|
||||
}
|
||||
}
|
||||
|
||||
function search($filter): \Traversable
|
||||
{
|
||||
foreach ($this->logs->where($filter)->order("id DESC") as $log)
|
||||
yield new Log($log);
|
||||
}
|
||||
|
||||
function getTypes(): array
|
||||
{
|
||||
$types = [];
|
||||
foreach ($this->context->query("SELECT DISTINCT(`object_model`) AS `object_model` FROM `ChandlerLogs`")->fetchAll() as $type)
|
||||
$types[] = str_replace(CHANDLER_ROOT_CONF["preferences"]["logs"]["entitiesNamespace"], "", $type->object_model);
|
||||
|
||||
return $types;
|
||||
}
|
||||
}
|
|
@ -56,7 +56,25 @@ CREATE TABLE `ChandlerUsers` (
|
|||
UNIQUE KEY `login` (`login`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE `ChandlerLogs` (
|
||||
`id` bigint(20) UNSIGNED NOT NULL,
|
||||
`user` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`type` int(11) NOT NULL,
|
||||
`object_table` tinytext COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`object_model` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`object_id` bigint(20) UNSIGNED NOT NULL,
|
||||
`xdiff_old` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`xdiff_new` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`ts` bigint(20) NOT NULL,
|
||||
`ip` tinytext COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`useragent` longtext COLLATE utf8mb4_unicode_ci NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
ALTER TABLE `ChandlerLogs`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
ALTER TABLE `ChandlerLogs`
|
||||
MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT;
|
||||
|
||||
|
||||
INSERT INTO `ChandlerGroups` VALUES ("c75fe4de-1e62-11ea-904d-42010aac0003", "Users", NULL);
|
||||
|
|
Loading…
Reference in a new issue