diff --git a/DBEntity.updated.php b/DBEntity.updated.php
new file mode 100644
index 00000000..5414a2e9
--- /dev/null
+++ b/DBEntity.updated.php
@@ -0,0 +1,135 @@
+getTable()->getName();
+ if($_table !== $this->tableName)
+ throw new ISE("Invalid data supplied for model: table $_table is not compatible with table" . $this->tableName);
+
+ $this->record = $row;
+ }
+
+ function __call(string $fName, array $args)
+ {
+ if(substr($fName, 0, 3) === "set") {
+ $field = mb_strtolower(substr($fName, 3));
+ $this->stateChanges($field, $args[0]);
+ } else {
+ throw new \Error("Call to undefined method " . get_class($this) . "::$fName");
+ }
+ }
+
+ private function getTable(): Selection
+ {
+ return DatabaseConnection::i()->getContext()->table($this->tableName);
+ }
+
+ protected function getRecord(): ?ActiveRow
+ {
+ return $this->record;
+ }
+
+ protected function stateChanges(string $column, $value): void
+ {
+ if(!is_null($this->record))
+ $t = $this->record->{$column}; #Test if column exists
+
+ $this->changes[$column] = $value;
+ }
+
+ function getId()
+ {
+ return $this->getRecord()->id;
+ }
+
+ function isDeleted(): bool
+ {
+ return (bool) $this->getRecord()->deleted;
+ }
+
+ function unwrap(): object
+ {
+ return (object) $this->getRecord()->toArray();
+ }
+
+ function delete(bool $softly = true): void
+ {
+ $user = CurrentUser::i()->getUser();
+ $user_id = is_null($user) ? (int) OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"] : $user->getId();
+
+ 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?");
+
+ (new Logs)->create($user_id, $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();
+ $this->deleted = true;
+ }
+ }
+
+ function undelete(): void
+ {
+ 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?");
+
+ $user = CurrentUser::i()->getUser();
+ $user_id = is_null($user) ? (int) OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"] : $user->getId();
+
+ (new Logs)->create($user_id, $this->getTable()->getName(), get_class($this), 3, $this->record->toArray(), ["deleted" => false]);
+
+ $this->getTable()->where("id", $this->record->id)->update(["deleted" => false]);
+ }
+
+ function save(?bool $log = true): void
+ {
+ if ($log) {
+ $user = CurrentUser::i();
+ $user_id = is_null($user) ? (int)OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"] : $user->getUser()->getId();
+ }
+
+ if(is_null($this->record)) {
+ $this->record = $this->getTable()->insert($this->changes);
+
+ if ($log && $this->getTable()->getName() !== "logs") {
+ (new Logs)->create($user_id, $this->getTable()->getName(), get_class($this), 0, $this->record->toArray(), $this->changes);
+ }
+ } else {
+ if ($log && $this->getTable()->getName() !== "logs") {
+ (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 = [];
+ }
+
+ use \Nette\SmartObject;
+}
diff --git a/Web/Models/Entities/Comment.php b/Web/Models/Entities/Comment.php
index 7c6222e4..d813d6be 100644
--- a/Web/Models/Entities/Comment.php
+++ b/Web/Models/Entities/Comment.php
@@ -85,4 +85,9 @@ class Comment extends Post
}
return $res;
}
+
+ function getURL(): string
+ {
+ return "/wall" . $this->getTarget()->getPrettyId() . "#_comment" . $this->getId();
+ }
}
diff --git a/Web/Models/Entities/IP.php b/Web/Models/Entities/IP.php
index ecea92ca..df2c9787 100644
--- a/Web/Models/Entities/IP.php
+++ b/Web/Models/Entities/IP.php
@@ -92,7 +92,7 @@ class IP extends RowModel
$this->stateChanges("rate_limit_counter", $aCounter);
$this->stateChanges("rate_limit_violation_counter_start", $vCounterSessionStart);
$this->stateChanges("rate_limit_violation_counter", $vCounter);
- $this->save();
+ $this->save(false);
}
}
@@ -105,11 +105,11 @@ class IP extends RowModel
$this->stateChanges("ip", $ip);
}
- function save(): void
+ function save($log): void
{
if(is_null($this->getRecord()))
$this->stateChanges("first_seen", time());
- parent::save();
+ parent::save($log);
}
}
diff --git a/Web/Models/Entities/Log.php b/Web/Models/Entities/Log.php
new file mode 100644
index 00000000..eabfd36f
--- /dev/null
+++ b/Web/Models/Entities/Log.php
@@ -0,0 +1,190 @@
+getRecord()->id;
+ }
+
+ function getUser(): ?User
+ {
+ return (new Users)->get((int) $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 ["добавил", "отредактировал", "удалил", "восстановил"][$this->getTypeRaw()];
+ }
+
+ function getTypeNom(): string
+ {
+ return ["Создание", "Редактирование", "Удаление", "Восстановление"][$this->getTypeRaw()];
+ }
+
+ function getObjectType(): string
+ {
+ return [
+ "albums" => "Альбом",
+ "groups" => "Сообщество",
+ "profiles" => "Профиль",
+ "comments" => "Комментарий",
+ "ip" => "IP-адрес",
+ "posts" => "Запись",
+ "tickets" => "Вопрос",
+ "tickets_comments" => "Комментарий к тикету",
+ ][$this->getRecord()->object_table] ?? $this->getRecord()->object_model;
+ }
+
+ 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(): DateTime
+ {
+ return new DateTime($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']) ? "" . implode(' ', $k['d']) . " " : '') .
+ (!empty($k['i']) ? "" . implode(' ', $k['i']) . " " : '');
+ 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);
+ $_changes[$field] = [
+ "field" => $field,
+ "old_value" => $result[$field],
+ "new_value" => strlen($new_value) > 0 ? $new_value : "(empty)",
+ "ts" => $this->getTime(),
+ "diff" => $this->htmlDiff((string) $result[$field], (string) $new_value)
+ ];
+ }
+ } 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;
+ }
+}
diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php
index ffc30a51..6b8c1051 100644
--- a/Web/Models/Entities/User.php
+++ b/Web/Models/Entities/User.php
@@ -1086,7 +1086,7 @@ class User extends RowModel
{
$this->setOnline(time());
$this->setClient_name($platform);
- $this->save();
+ $this->save(false);
return true;
}
diff --git a/Web/Models/Repositories/CurrentUser.php b/Web/Models/Repositories/CurrentUser.php
new file mode 100644
index 00000000..c6cb942b
--- /dev/null
+++ b/Web/Models/Repositories/CurrentUser.php
@@ -0,0 +1,49 @@
+user = $user;
+
+ if ($ip)
+ $this->ip = $ip;
+
+ if ($useragent)
+ $this->useragent = $useragent;
+ }
+
+ public static function get($user, $ip, $useragent)
+ {
+ if (self::$instance === null) self::$instance = new self($user, $ip, $useragent);
+ return self::$instance;
+ }
+
+ public function getUser(): User
+ {
+ return $this->user;
+ }
+
+ public function getIP(): string
+ {
+ return $this->ip;
+ }
+
+ public function getUserAgent(): string
+ {
+ return $this->useragent;
+ }
+
+ public static function i()
+ {
+ return self::$instance;
+ }
+}
diff --git a/Web/Models/Repositories/IPs.php b/Web/Models/Repositories/IPs.php
index c4485b73..59fc6570 100644
--- a/Web/Models/Repositories/IPs.php
+++ b/Web/Models/Repositories/IPs.php
@@ -24,7 +24,7 @@ class IPs
if(!$res) {
$res = new IP;
$res->setIp($ip);
- $res->save();
+ $res->save(false);
return $res;
}
diff --git a/Web/Models/Repositories/Logs.php b/Web/Models/Repositories/Logs.php
new file mode 100644
index 00000000..6f7d3937
--- /dev/null
+++ b/Web/Models/Repositories/Logs.php
@@ -0,0 +1,96 @@
+context = DatabaseConnection::i()->getContext();
+ $this->logs = $this->context->table("logs");
+ }
+
+ 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(int $user, string $table, string $model, int $type, $object, $changes): void
+ {
+ if (OPENVK_ROOT_CONF["openvk"]["preferences"]["logs"] === true) {
+ $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 find(string $query, array $pars = [], string $sort = "id DESC", int $page = 1, ?int $perPage = NULL): \Traversable
+ {
+ $query = "%$query%";
+ $result = $this->logs->where("id LIKE ? OR object_table LIKE ?", $query, $query);
+
+ return new Util\EntityStream("Log", $result->order($sort));
+ }
+
+ 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 `logs`")->fetchAll() as $type)
+ $types[] = str_replace("openvk\\Web\\Models\\Entities\\", "", $type->object_model);
+
+ return $types;
+ }
+}
diff --git a/Web/Presenters/AdminPresenter.php b/Web/Presenters/AdminPresenter.php
index 15c80e5f..8ad00380 100644
--- a/Web/Presenters/AdminPresenter.php
+++ b/Web/Presenters/AdminPresenter.php
@@ -1,7 +1,7 @@
users = $users;
$this->clubs = $clubs;
@@ -21,6 +22,7 @@ final class AdminPresenter extends OpenVKPresenter
$this->gifts = $gifts;
$this->bannedLinks = $bannedLinks;
$this->chandlerGroups = $chandlerGroups;
+ $this->logs = $logs;
parent::__construct();
}
@@ -572,4 +574,43 @@ final class AdminPresenter extends OpenVKPresenter
$this->redirect("/admin/users/id" . $user->getId());
}
+
+ function renderLogs(): void
+ {
+ $filter = [];
+
+ if ($this->queryParam("id")) {
+ $id = (int) $this->queryParam("id");
+ $filter["id"] = $id;
+ $this->template->id = $id;
+ }
+ if ($this->queryParam("type") !== NULL && $this->queryParam("type") !== "any") {
+ $type = in_array($this->queryParam("type"), [0, 1, 2, 3]) ? (int) $this->queryParam("type") : 0;
+ $filter["type"] = $type;
+ $this->template->type = $type;
+ }
+ if ($this->queryParam("uid")) {
+ $user = (int) $this->queryParam("uid");
+ $filter["user"] = $user;
+ $this->template->user = $user;
+ }
+ if ($this->queryParam("obj_id")) {
+ $obj_id = (int) $this->queryParam("obj_id");
+ $filter["object_id"] = $obj_id;
+ $this->template->obj_id = $obj_id;
+ }
+ if ($this->queryParam("obj_type") !== NULL && $this->queryParam("obj_type") !== "any") {
+ $obj_type = "openvk\\Web\\Models\\Entities\\" . $this->queryParam("obj_type");
+ $filter["object_model"] = $obj_type;
+ $this->template->obj_type = $obj_type;
+ }
+
+ if (count($filter) === 0) {
+ $this->template->logs = $this->searchResults($this->logs, $this->template->count);
+ } else {
+ $this->template->logs = $this->logs->search($filter);
+ }
+
+ $this->template->object_types = (new Logs)->getTypes();
+ }
}
diff --git a/Web/Presenters/AuthPresenter.php b/Web/Presenters/AuthPresenter.php
index 7a879dd2..52113bcf 100644
--- a/Web/Presenters/AuthPresenter.php
+++ b/Web/Presenters/AuthPresenter.php
@@ -1,7 +1,7 @@
flashFail("err", tr("failed_to_register"), tr("user_already_exists"));
$user->setUser($chUser->getId());
- $user->save();
+ $user->save(false);
if(!is_null($referer)) {
$user->toggleSubscription($referer);
@@ -130,7 +130,9 @@ final class AuthPresenter extends OpenVKPresenter
}
$this->authenticator->authenticate($chUser->getId());
+ (new Logs)->create($user->getId(), "profiles", "openvk\\Web\\Models\\Entities\\User", 0, $user, $user);
$this->redirect("/id" . $user->getId());
+ $user->save();
}
}
diff --git a/Web/Presenters/OpenVKPresenter.php b/Web/Presenters/OpenVKPresenter.php
old mode 100755
new mode 100644
index 5edc870a..80ab0621
--- a/Web/Presenters/OpenVKPresenter.php
+++ b/Web/Presenters/OpenVKPresenter.php
@@ -7,7 +7,7 @@ use Chandler\Security\Authenticator;
use Latte\Engine as TemplatingEngine;
use openvk\Web\Models\Entities\IP;
use openvk\Web\Themes\Themepacks;
-use openvk\Web\Models\Repositories\{IPs, Users, APITokens, Tickets, Reports};
+use openvk\Web\Models\Repositories\{IPs, Users, APITokens, Tickets, Reports, CurrentUser};
use WhichBrowser;
abstract class OpenVKPresenter extends SimplePresenter
@@ -211,6 +211,7 @@ abstract class OpenVKPresenter extends SimplePresenter
$this->user->id = $this->user->identity->getId();
$this->template->thisUser = $this->user->identity;
$this->template->userTainted = $user->isTainted();
+ CurrentUser::get($this->user->identity, $_SERVER["REMOTE_ADDR"], $_SERVER["HTTP_USER_AGENT"]);
if($this->user->identity->isDeleted() && !$this->deactivationTolerant) {
if($this->user->identity->isDeactivated()) {
@@ -255,7 +256,7 @@ abstract class OpenVKPresenter extends SimplePresenter
if($this->user->identity->onlineStatus() == 0 && !($this->user->identity->isDeleted() || $this->user->identity->isBanned())) {
$this->user->identity->setOnline(time());
$this->user->identity->setClient_name(NULL);
- $this->user->identity->save();
+ $this->user->identity->save(false);
}
$this->template->ticketAnsweredCount = (new Tickets)->getTicketsCountByUserId($this->user->id, 1);
diff --git a/Web/Presenters/templates/Admin/@layout.xml b/Web/Presenters/templates/Admin/@layout.xml
index 7b1a30f3..f254dd9a 100644
--- a/Web/Presenters/templates/Admin/@layout.xml
+++ b/Web/Presenters/templates/Admin/@layout.xml
@@ -124,6 +124,9 @@
ID | +Пользователь | +Объект | +Тип | +Изменения | +Время | +
---|---|---|---|---|---|
{$log->getId()} | +
+
+
+ |
+
+
+
+ |
+ {$log->getTypeNom()} | +
+ {foreach $log->getChanges() as $change}
+
+ {$change["field"]}:
+ {if array_key_exists('diff', $change)}
+ {$change["diff"]|noescape}
+ {else}
+ {$change["old_value"]}
+ {/if}
+
+ {/foreach}
+ |
+ + {$change["ts"]} + | +