This commit is contained in:
n1rwana 2023-08-02 16:56:07 +03:00
parent a2384cc231
commit fab54f37c8
18 changed files with 639 additions and 11 deletions

135
DBEntity.updated.php Normal file
View file

@ -0,0 +1,135 @@
<?php declare(strict_types=1);
namespace Chandler\Database;
use Chandler\Database\DatabaseConnection;
use Nette\Database\Table\Selection;
use Nette\Database\Table\ActiveRow;
use Nette\InvalidStateException as ISE;
use openvk\Web\Models\Repositories\CurrentUser;
use openvk\Web\Models\Repositories\Logs;
abstract class DBEntity
{
protected $record;
protected $changes;
protected $deleted;
protected $user;
protected $tableName;
function __construct(?ActiveRow $row = NULL)
{
if(is_null($row)) return;
$_table = $row->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()->getUser();
$user_id = is_null($user) ? (int)OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"] : $user->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;
}

View file

@ -85,4 +85,9 @@ class Comment extends Post
} }
return $res; return $res;
} }
function getURL(): string
{
return "/wall" . $this->getTarget()->getPrettyId() . "#_comment" . $this->getId();
}
} }

View file

@ -92,7 +92,7 @@ class IP extends RowModel
$this->stateChanges("rate_limit_counter", $aCounter); $this->stateChanges("rate_limit_counter", $aCounter);
$this->stateChanges("rate_limit_violation_counter_start", $vCounterSessionStart); $this->stateChanges("rate_limit_violation_counter_start", $vCounterSessionStart);
$this->stateChanges("rate_limit_violation_counter", $vCounter); $this->stateChanges("rate_limit_violation_counter", $vCounter);
$this->save(); $this->save(false);
} }
} }
@ -105,11 +105,11 @@ class IP extends RowModel
$this->stateChanges("ip", $ip); $this->stateChanges("ip", $ip);
} }
function save(): void function save($log): void
{ {
if(is_null($this->getRecord())) if(is_null($this->getRecord()))
$this->stateChanges("first_seen", time()); $this->stateChanges("first_seen", time());
parent::save(); parent::save($log);
} }
} }

190
Web/Models/Entities/Log.php Normal file
View file

@ -0,0 +1,190 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Repositories\GeodbCities;
use openvk\Web\Models\Repositories\GeodbCountries;
use openvk\Web\Models\Repositories\GeodbEducation;
use openvk\Web\Models\Repositories\GeodbSpecializations;
use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\RowModel;
use openvk\Web\Util\DateTime;
class Log extends RowModel
{
protected $tableName = "logs";
function getId(): int
{
return (int) $this->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']) ? "<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);
$_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;
}
}

View file

@ -1013,7 +1013,7 @@ class User extends RowModel
{ {
$this->setOnline(time()); $this->setOnline(time());
$this->setClient_name($platform); $this->setClient_name($platform);
$this->save(); $this->save(false);
return true; return true;
} }
@ -1031,7 +1031,7 @@ class User extends RowModel
function adminNotify(string $message): bool function adminNotify(string $message): bool
{ {
$admId = OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"]; $admId = (int) OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"];
if(!$admId) if(!$admId)
return false; return false;
else if(is_null($admin = (new Users)->get($admId))) else if(is_null($admin = (new Users)->get($admId)))

View file

@ -0,0 +1,31 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use openvk\Web\Models\Entities\User;
class CurrentUser
{
private static $instance = null;
private $user;
public function __construct(?User $user = NULL)
{
if ($user)
$this->user = $user;
}
public static function get($user)
{
if (self::$instance === null) self::$instance = new self($user);
return self::$instance;
}
public function getUser(): User
{
return $this->user;
}
public static function i()
{
return self::$instance;
}
}

View file

@ -0,0 +1,94 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Chandler\Database\DatabaseConnection;
use Nette\Database\Table\ActiveRow;
use openvk\Web\Models\Entities\Country;
use openvk\Web\Models\Entities\Editor;
use openvk\Web\Models\Entities\Log;
use openvk\Web\Models\Entities\User;
class Logs
{
private $context;
private $logs;
function __construct()
{
$this->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->save(false);
}
}
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;
}
}

View file

@ -1,7 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Voucher, Gift, GiftCategory, User, BannedLink}; use openvk\Web\Models\Entities\{Voucher, Gift, GiftCategory, User, BannedLink};
use openvk\Web\Models\Repositories\{ChandlerGroups, ChandlerUsers, Users, Clubs, Vouchers, Gifts, BannedLinks}; use openvk\Web\Models\Repositories\{ChandlerGroups, ChandlerUsers, Logs, Users, Clubs, Vouchers, Gifts, BannedLinks};
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
final class AdminPresenter extends OpenVKPresenter final class AdminPresenter extends OpenVKPresenter
@ -12,8 +12,9 @@ final class AdminPresenter extends OpenVKPresenter
private $gifts; private $gifts;
private $bannedLinks; private $bannedLinks;
private $chandlerGroups; private $chandlerGroups;
private $logs;
function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts, BannedLinks $bannedLinks, ChandlerGroups $chandlerGroups) function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts, BannedLinks $bannedLinks, ChandlerGroups $chandlerGroups, Logs $logs)
{ {
$this->users = $users; $this->users = $users;
$this->clubs = $clubs; $this->clubs = $clubs;
@ -21,6 +22,7 @@ final class AdminPresenter extends OpenVKPresenter
$this->gifts = $gifts; $this->gifts = $gifts;
$this->bannedLinks = $bannedLinks; $this->bannedLinks = $bannedLinks;
$this->chandlerGroups = $chandlerGroups; $this->chandlerGroups = $chandlerGroups;
$this->logs = $logs;
parent::__construct(); parent::__construct();
} }
@ -550,4 +552,43 @@ final class AdminPresenter extends OpenVKPresenter
$this->redirect("/admin/users/id" . $user->getId()); $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();
}
} }

View file

@ -1,7 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{IP, User, PasswordReset, EmailVerification}; use openvk\Web\Models\Entities\{IP, User, PasswordReset, EmailVerification};
use openvk\Web\Models\Repositories\{IPs, Users, Restores, Verifications}; use openvk\Web\Models\Repositories\{IPs, Logs, Users, Restores, Verifications};
use openvk\Web\Models\Exceptions\InvalidUserNameException; use openvk\Web\Models\Exceptions\InvalidUserNameException;
use openvk\Web\Util\Validator; use openvk\Web\Util\Validator;
use Chandler\Session\Session; use Chandler\Session\Session;
@ -110,7 +110,7 @@ final class AuthPresenter extends OpenVKPresenter
$this->flashFail("err", tr("failed_to_register"), tr("user_already_exists")); $this->flashFail("err", tr("failed_to_register"), tr("user_already_exists"));
$user->setUser($chUser->getId()); $user->setUser($chUser->getId());
$user->save(); $user->save(false);
if(!is_null($referer)) { if(!is_null($referer)) {
$user->toggleSubscription($referer); $user->toggleSubscription($referer);
@ -130,7 +130,9 @@ final class AuthPresenter extends OpenVKPresenter
} }
$this->authenticator->authenticate($chUser->getId()); $this->authenticator->authenticate($chUser->getId());
(new Logs)->create($user->getId(), "profiles", "openvk\\Web\\Models\\Entities\\User", 0, $user, $user);
$this->redirect("/id" . $user->getId()); $this->redirect("/id" . $user->getId());
$user->save();
} }
} }

View file

@ -7,7 +7,7 @@ use Chandler\Security\Authenticator;
use Latte\Engine as TemplatingEngine; use Latte\Engine as TemplatingEngine;
use openvk\Web\Models\Entities\IP; use openvk\Web\Models\Entities\IP;
use openvk\Web\Themes\Themepacks; use openvk\Web\Themes\Themepacks;
use openvk\Web\Models\Repositories\{IPs, Users, APITokens, Tickets}; use openvk\Web\Models\Repositories\{CurrentUser, IPs, Users, APITokens, Tickets};
use WhichBrowser; use WhichBrowser;
abstract class OpenVKPresenter extends SimplePresenter abstract class OpenVKPresenter extends SimplePresenter
@ -211,6 +211,7 @@ abstract class OpenVKPresenter extends SimplePresenter
$this->user->id = $this->user->identity->getId(); $this->user->id = $this->user->identity->getId();
$this->template->thisUser = $this->user->identity; $this->template->thisUser = $this->user->identity;
$this->template->userTainted = $user->isTainted(); $this->template->userTainted = $user->isTainted();
CurrentUser::get($this->user->identity);
if($this->user->identity->isDeleted() && !$this->deactivationTolerant) { if($this->user->identity->isDeleted() && !$this->deactivationTolerant) {
if($this->user->identity->isDeactivated()) { 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())) { if($this->user->identity->onlineStatus() == 0 && !($this->user->identity->isDeleted() || $this->user->identity->isBanned())) {
$this->user->identity->setOnline(time()); $this->user->identity->setOnline(time());
$this->user->identity->setClient_name(NULL); $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); $this->template->ticketAnsweredCount = (new Tickets)->getTicketsCountByUserId($this->user->id, 1);

View file

@ -124,6 +124,9 @@
<li> <li>
<a href="/admin/settings/tuning">{_admin_settings_tuning}</a> <a href="/admin/settings/tuning">{_admin_settings_tuning}</a>
</li> </li>
<li>
<a href="/admin/logs">Логи</a>
</li>
<li> <li>
<a href="/admin/settings/appearance">{_admin_settings_appearance}</a> <a href="/admin/settings/appearance">{_admin_settings_appearance}</a>
</li> </li>

View file

@ -0,0 +1,99 @@
{extends "@layout.xml"}
{block title}
Логи
{/block}
{block heading}
Логи
{/block}
{block content}
{var $logs = iterator_to_array($logs)}
{var $amount = sizeof($logs)}
<style>
del, ins { text-decoration: none; color: #000; }
del { background: #fdd; }
ins { background: #dfd; }
</style>
<form class="aui">
<div>
<select class="select medium-field" type="number" id="type" name="type" placeholder="Тип изменения">
<option value="any" n:attr="selected => !$type">Любое</option>
<option value="0" n:attr="selected => $type === 0">Создание</option>
<option value="1" n:attr="selected => $type === 1">Редактирование</option>
<option value="2" n:attr="selected => $type === 2">Удаление</option>
<option value="3" n:attr="selected => $type === 3">Восстановление</option>
</select>
<input class="text medium-field" type="number" id="id" name="id" placeholder="ID записи" n:attr="value => $id"/>
<input class="text medium-field" type="number" id="uid" name="uid" placeholder="ID пользователя" n:attr="value => $user"/>
</div>
<div style="margin: 8px 0;" />
<div>
<select class="select medium-field" id="obj_type" name="obj_type" placeholder="Тип объекта">
<option value="any" n:attr="selected => !$obj_type">Любой</option>
<option n:foreach="$object_types as $type" n:attr="selected => $obj_type === $type">{$type}</option>
</select>
<input class="text medium-field" type="number" id="obj_id" name="obj_id" placeholder="ID объекта" n:attr="value => $obj_id"/>
<input type="submit" class="aui-button aui-button-primary medium-field" value="Поиск" style="width: 165px;"/>
</div>
</form>
<table class="aui aui-table-list">
<thead>
<tr>
<th>ID</th>
<th>Пользователь</th>
<th>Объект</th>
<th>Тип</th>
<th>Изменения</th>
<th>Время</th>
</tr>
</thead>
<tbody>
<tr n:foreach="$logs as $log">
<td>{$log->getId()}</td>
<td>
<span n:if="$log->getUser()->getAvatarURL()" class="aui-avatar aui-avatar-xsmall">
<span class="aui-avatar-inner">
<img src="{$log->getUser()->getAvatarURL()}" alt="{$log->getUser()->getCanonicalName()}" style="object-fit: cover;" role="presentation" />
</span>
</span>
<a href="{$log->getUser()->getURL()}">{$log->getUser()->getCanonicalName()}</a>
</td>
<td>
<span n:if="$log->getObjectAvatar()" class="aui-avatar aui-avatar-xsmall">
<span class="aui-avatar-inner">
<img src="{$log->getObjectAvatar()}" alt="{$log->getObjectName()}" style="object-fit: cover;" role="presentation" />
</span>
</span>
<a href="{$log->getObjectURL()}">{$log->getObjectName()}</a>
</td>
<td>{$log->getTypeNom()}</td>
<td>
{foreach $log->getChanges() as $change}
<div>
<b>{$change["field"]}</b>:
{if array_key_exists('diff', $change)}
{$change["diff"]|noescape}
{else}
<ins>{$change["old_value"]}</ins>
{/if}
</div>
{/foreach}
</td>
<td>
{$change["ts"]}
</td>
</tr>
</tbody>
</table>
<br/>
<div align="right">
{var $isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + $amount) < $count}
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?p={($_GET['p'] ?? 1) - 1}">&laquo;</a>
<a n:if="$isLast" class="aui-button" href="?p={($_GET['p'] ?? 1) + 1}">&raquo;</a>
</div>
{/block}

View file

@ -124,6 +124,7 @@
{/if} {/if}
{if $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)} {if $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)}
<a href="/admin/clubs/id{$club->getId()}" id="profile_link">{_manage_group_action}</a> <a href="/admin/clubs/id{$club->getId()}" id="profile_link">{_manage_group_action}</a>
<a href="/admin/logs?obj_id={$club->getId()}&obj_type=Club" class="profile_link">Последние действия</a>
{/if} {/if}
{if $club->getSubscriptionStatus($thisUser) == false} {if $club->getSubscriptionStatus($thisUser) == false}
<form action="/setSub/club" method="post"> <form action="/setSub/club" method="post">

View file

@ -118,6 +118,9 @@
<a href="javascript:warnUser()" class="profile_link" style="width: 194px;"> <a href="javascript:warnUser()" class="profile_link" style="width: 194px;">
{_warn_user_action} {_warn_user_action}
</a> </a>
<a href="/admin/logs?uid={$user->getId()}" class="profile_link" style="width: 194px;">
Последние действия
</a>
{/if} {/if}
{if $thisUser->getChandlerUser()->can('write')->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)} {if $thisUser->getChandlerUser()->can('write')->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)}

View file

@ -49,3 +49,4 @@ services:
- openvk\Web\Models\Repositories\BannedLinks - openvk\Web\Models\Repositories\BannedLinks
- openvk\Web\Models\Repositories\ChandlerGroups - openvk\Web\Models\Repositories\ChandlerGroups
- openvk\Web\Presenters\MaintenancePresenter - openvk\Web\Presenters\MaintenancePresenter
- openvk\Web\Models\Repositories\Logs

View file

@ -339,6 +339,8 @@ routes:
handler: "Admin->chandlerGroup" handler: "Admin->chandlerGroup"
- url: "/admin/chandler/users/{slug}" - url: "/admin/chandler/users/{slug}"
handler: "Admin->chandlerUser" handler: "Admin->chandlerUser"
- url: "/admin/logs"
handler: "Admin->logs"
- url: "/internal/wall{num}" - url: "/internal/wall{num}"
handler: "Wall->wallEmbedded" handler: "Wall->wallEmbedded"
- url: "/robots.txt" - url: "/robots.txt"

19
install/00038-logs.sql Normal file
View file

@ -0,0 +1,19 @@
CREATE TABLE `logs`
(
`id` bigint(20) UNSIGNED NOT NULL,
`user` bigint(20) UNSIGNED 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
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
ALTER TABLE `logs`
ADD PRIMARY KEY (`id`);
ALTER TABLE `logs`
MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT;
COMMIT;

View file

@ -102,6 +102,7 @@ openvk:
fartscroll: false fartscroll: false
testLabel: false testLabel: false
defaultMobileTheme: "" defaultMobileTheme: ""
logs: true
telemetry: telemetry:
plausible: plausible: