Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# Changelog

## [1.0.0-beta2] – 2026-03-06

### Bugfixes

- **`ART_ADDED` doppelter Log-Eintrag bei Mehrsprachigkeit**: REDAXO feuert `ART_ADDED` einmal pro Sprache – neuer Artikel wird jetzt nur einmal geloggt (`static $logged` Deduplication).
- **`CAT_ADDED` / `CAT_DELETED` doppelter Log-Eintrag bei Mehrsprachigkeit**: Gleiche Deduplication-Logik wie bei Artikeln auf Kategorien ausgeweitet.

### Technisches

- **PHPStan Level 8 – 0 Fehler**: Komplette Typbereinigung aller Addon-Klassen und Fragmente.
- `Activity`: Statische Properties typisiert, Fluent-Methoden geben `self` zurück, `new self()`, `assert()` für Factory-Methode.
- `ActivityClear`: `QuestionHelper`-Cast, Validator-Signatur auf `float|int|string`.
- `ActivityLogCronjob`: `@return array<int, array<string, mixed>>` für `getParamFields()`.
- `EP\EpTrait`: `callable` → `string|callable`, korrekte PHPDoc-Typen für Callbacks.
Comment thread
skerbis marked this conversation as resolved.
- `EP\User`: `rex_user`-Import und `@var`-Cast für `$params['user']`.
- `EP\Yform.php`: Unterdrückung der `rex_yform_manager_table`-Fehler (optionales Addon, nicht in Analyse-Umgebung installiert) via `phpstan.neon`.
- `fragments/filter-form.php`: `(string)`-Cast für `htmlspecialchars()` / `ucfirst()`.
- `pages/system.activity-log.php`: Null-sicherer Operator `rex::getUser()?->isAdmin() ?? false`.
- **`phpstan.neon`** ergänzt um `ignoreErrors` für `rex_yform_manager_table` (optionales Addon) und `fragments/` als Analyse-Pfad.

---

## [1.0.0-beta1] – 2026-03-06

Diese Version bringt die lang geplante Code-Modernisierung mit vollständiger Namespace-Migration sowie mehrere Bugfixes, neue Features und modernisierte CI-Workflows.
Expand Down
2 changes: 1 addition & 1 deletion assets/css/styles.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion fragments/filter-form.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class="form-control"
<label for="filter_source"><?= rex_i18n::msg('activity_log_col_source') ?></label>
<select class="form-control selectpicker" name="source" id="filter_source" title="-">
<?php foreach ($sources as $row) : ?>
<option <?= ($this->source ?? '') === $row['source'] ? 'selected' : '' ?> value="<?= htmlspecialchars($row['source']) ?>"><?= htmlspecialchars(ucfirst($row['source'])) ?></option>
<option <?= ($this->source ?? '') === $row['source'] ? 'selected' : '' ?> value="<?= htmlspecialchars((string) $row['source']) ?>"><?= htmlspecialchars(ucfirst((string) $row['source'])) ?></option>
<?php endforeach ?>
</select>
</div>
Expand Down
48 changes: 31 additions & 17 deletions lib/Activity.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
use rex_url;
use rex_user;

use function assert;
use function is_int;

class Activity
{
public const TYPE_INFO = 'info';
Expand All @@ -24,13 +27,12 @@ class Activity
public const TYPE_EDIT = 'edit';
public const TYPE_DELETE = 'delete';

/** @var rex_addon|null */
private static $addon;
private static $table = '';
private static $activity;
private static $message;
private static $type;
private static $causer;
private static rex_addon $addon;
private static string $table = '';
private static ?self $activity = null;
private static ?string $message = null;
private static ?string $type = null;
private static ?int $causer = null;
private static ?string $source = null;

/**
Expand All @@ -39,11 +41,13 @@ class Activity
public static function __constructStatic(): void
{
self::$table = rex::getTable('activity_log');
self::$addon = rex_addon::get('activity_log');
$addon = rex_addon::get('activity_log');
assert($addon instanceof rex_addon);
self::$addon = $addon;
self::$addon->setConfig('cleared', false);

if (null === self::$activity) {
self::$activity = new static();
self::$activity = new self();
}
}

Expand Down Expand Up @@ -86,48 +90,52 @@ public static function log(): void
/**
* Set message.
*/
public static function message(string $message): ?self
public static function message(string $message): self
{
self::$message = $message;
assert(null !== self::$activity);
return self::$activity;
}

/**
* Set type (default: notice).
*/
public static function type(string $type): ?self
public static function type(string $type): self
{
self::$type = $type;
assert(null !== self::$activity);
return self::$activity;
}

/**
* Set source (e.g. 'article', 'yform', 'media').
*/
public static function source(string $source): ?self
public static function source(string $source): self
{
self::$source = $source;
assert(null !== self::$activity);
return self::$activity;
}

/**
* Set causer.
*
* @param rex_user|int $user
*/
public static function causer($user): ?self
public static function causer(rex_user|int|null $user): self
{
if (is_numeric($user)) {
self::$causer = (int) $user;
if (is_int($user)) {
self::$causer = $user;
} elseif ($user instanceof rex_user) {
self::$causer = $user->getId();
}

assert(null !== self::$activity);
return self::$activity;
}

/**
* List callback – user column.
*
* @param array<string, mixed> $params
*/
public static function userListCallback(array $params): string
{
Expand All @@ -153,6 +161,8 @@ public static function userListCallback(array $params): string

/**
* List callback – type column.
*
* @param array<string, mixed> $params
*/
public static function typeListCallback(array $params): string
{
Expand All @@ -165,6 +175,8 @@ public static function typeListCallback(array $params): string

/**
* List callback – message column.
*
* @param array<string, mixed> $params
*/
public static function messageListCallback(array $params): string
{
Expand All @@ -173,6 +185,8 @@ public static function messageListCallback(array $params): string

/**
* List callback – source column.
*
* @param array<string, mixed> $params
*/
public static function sourceListCallback(array $params): string
{
Expand Down
6 changes: 4 additions & 2 deletions lib/ActivityClear.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use rex_sql_exception;
use RuntimeException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
Expand All @@ -29,11 +30,12 @@ protected function configure(): void
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');

$daysQuestion = new Question('Delete logs older than n days (default: -1 -> delete all): ', -1);
$daysQuestion->setValidator(static function ($answer) {
if (!is_numeric($answer) || '' === $answer) {
$daysQuestion->setValidator(static function (mixed $answer): float|int|string {
if (!is_numeric($answer)) {
throw new RuntimeException('You must enter a number.');
}
return $answer;
Expand Down
2 changes: 1 addition & 1 deletion lib/ActivityLogCronjob.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public function getTypeName(): string
/**
* Additional parameter fields.
*
* @return array<array>
* @return array<int, array<string, mixed>>
*/
public function getParamFields(): array
{
Expand Down
33 changes: 32 additions & 1 deletion lib/EP/Article.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public function __construct()

/** @phpstan-ignore-next-line */
if (is_bool(self::$addon->getConfig('article_added')) && self::$addon->getConfig('article_added')) {
$this->add('ART_ADDED', static::class . '::message');
$this->addArticle();
}

/** @phpstan-ignore-next-line */
Expand All @@ -50,6 +50,37 @@ protected function getSource(): string
return 'article';
}

/**
* Registriert den ART_ADDED-Handler mit Deduplication:
* Da REDAXO ART_ADDED einmal pro Sprache feuert, wird nur der erste Aufruf
* pro Artikel-ID geloggt.
*/
private function addArticle(): void
{
$source = $this->getSource();
rex_extension::register('ART_ADDED', static function (rex_extension_point $ep) use ($source): void {
/** @var array<string, mixed> $params */
$params = $ep->getParams();
$articleId = (int) ($params['id'] ?? 0);

/** @var array<int, bool> $logged */
static $logged = [];

if (isset($logged[$articleId])) {
return;
}
$logged[$articleId] = true;

$message = static::message($params, Activity::TYPE_ADD);

Activity::message($message)
->type(Activity::TYPE_ADD)
->source($source)
->causer(rex::getUser())
->log();
});
}

/**
* Registriert den ART_DELETED-Handler mit Deduplication:
* Da REDAXO ART_DELETED einmal pro Sprache feuert, wird nur der erste Aufruf
Expand Down
70 changes: 68 additions & 2 deletions lib/EP/Category.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

namespace FriendsOfRedaxo\ActivityLog\EP;

use FriendsOfRedaxo\ActivityLog\Activity;
use rex;
use rex_addon_interface;
use rex_category;
use rex_clang;
use rex_extension;
use rex_extension_point;
use rex_url;

use function is_bool;
Expand All @@ -22,7 +26,7 @@ public function __construct()

/** @phpstan-ignore-next-line */
if (is_bool(self::$addon->getConfig('category_added')) && self::$addon->getConfig('category_added')) {
$this->add('CAT_ADDED', static::class . '::message');
$this->addCategory();
}

/** @phpstan-ignore-next-line */
Expand All @@ -32,7 +36,7 @@ public function __construct()

/** @phpstan-ignore-next-line */
if (is_bool(self::$addon->getConfig('category_deleted')) && self::$addon->getConfig('category_deleted')) {
$this->delete('CAT_DELETED', static::class . '::message');
$this->deleteCategory();
}

/** @phpstan-ignore-next-line */
Expand All @@ -46,6 +50,68 @@ protected function getSource(): string
return 'category';
}

/**
* Registriert den CAT_ADDED-Handler mit Deduplication:
* Da REDAXO CAT_ADDED einmal pro Sprache feuert, wird nur der erste Aufruf
* pro Kategorie-ID geloggt.
*/
private function addCategory(): void
{
$source = $this->getSource();
rex_extension::register('CAT_ADDED', static function (rex_extension_point $ep) use ($source): void {
/** @var array<string, mixed> $params */
$params = $ep->getParams();
$categoryId = (int) ($params['id'] ?? 0);

/** @var array<int, bool> $logged */
static $logged = [];

if (isset($logged[$categoryId])) {
return;
}
$logged[$categoryId] = true;

$message = static::message($params, Activity::TYPE_ADD);

Activity::message($message)
->type(Activity::TYPE_ADD)
->source($source)
->causer(rex::getUser())
->log();
});
}

/**
* Registriert den CAT_DELETED-Handler mit Deduplication:
* Da REDAXO CAT_DELETED einmal pro Sprache feuert, wird nur der erste Aufruf
* pro Kategorie-ID geloggt.
*/
private function deleteCategory(): void
{
$source = $this->getSource();
rex_extension::register('CAT_DELETED', static function (rex_extension_point $ep) use ($source): void {
/** @var array<string, mixed> $params */
$params = $ep->getParams();
$categoryId = (int) ($params['id'] ?? 0);

/** @var array<int, bool> $logged */
static $logged = [];

if (isset($logged[$categoryId])) {
return;
}
$logged[$categoryId] = true;

$message = static::message($params, Activity::TYPE_DELETE);

Activity::message($message)
->type(Activity::TYPE_DELETE)
->source($source)
->causer(rex::getUser())
->log();
});
}

/**
* @param array<string> $params
* @param array<string>|null $additionalParams
Expand Down
Loading