<permissions>admin.management.canViewLog</permissions>
<options>enable_user_authentication_failure</options>
</acpmenuitem>
+
+ <acpmenuitem name="wcf.acp.menu.link.log.modification">
+ <controller>wcf\acp\page\ModificationLogListPage</controller>
+ <parent>wcf.acp.menu.link.log</parent>
+ <permissions>admin.management.canViewLog</permissions>
+ </acpmenuitem>
<!-- /log -->
<!-- /management -->
<definition>
<name>com.woltlab.wcf.modifiableContent</name>
+ <interfacename>wcf\system\log\modification\IExtendedModificationLogHandler</interfacename>
</definition>
<definition>
--- /dev/null
+{include file='header' pageTitle='wcf.acp.modificationLog.list'}
+
+<header class="contentHeader">
+ <div class="contentHeaderTitle">
+ <h1 class="contentTitle">{lang}wcf.acp.modificationLog.list{/lang} <span class="badge badgeInverse">{#$items}</span></h1>
+ </div>
+
+ {hascontent}
+ <nav class="contentHeaderNavigation">
+ <ul>
+ {content}{event name='contentHeaderNavigation'}{/content}
+ </ul>
+ </nav>
+ {/hascontent}
+</header>
+
+{if !$unsupportedObjectTypes|empty}
+ <div class="warning">
+ <p>{lang}wcf.acp.modificationLog.unsupportedObjectTypes{/lang}</p>
+ <ul class="nativeList">
+ {foreach from=$unsupportedObjectTypes item=unsupportedObjectType}
+ <li><kbd>{$unsupportedObjectType->objectType}</kbd> ({$unsupportedObjectType->getPackage()})</li>
+ {/foreach}
+ </ul>
+ </div>
+{/if}
+
+<form method="post" action="{link controller='ModificationLogList'}{/link}">
+ <section class="section">
+ <h2 class="sectionTitle">{lang}wcf.global.filter{/lang}</h2>
+
+ <div class="row rowColGap formGrid">
+ <dl class="col-xs-12 col-md-4">
+ <dt></dt>
+ <dd>
+ <input type="text" id="username" name="username" value="" placeholder="{lang}wcf.user.username{/lang}" class="long">
+ </dd>
+ </dl>
+
+ <dl class="col-xs-12 col-md-4">
+ <dt></dt>
+ <dd>
+ <select name="packageID" id="packageID">
+ <option value="0"{if $packageID === 0} selected{/if}>{lang}wcf.acp.modificationLog.package.all{/lang}</option>
+ {foreach from=$packages item=package}
+ <option value="{@$package->packageID}"{if $packageID == $package->packageID} selected{/if}>{$package}</option>
+ {/foreach}
+ </select>
+ </dd>
+ </dl>
+
+ <dl class="col-xs-12 col-md-4">
+ <dt></dt>
+ <dd>
+ <select name="action" id="action">
+ <option value=""{if $action === ''} selected{/if}>{lang}wcf.acp.modificationLog.action.all{/lang}</option>
+ {if !$actions|empty}<option disabled>-----</option>{/if}
+
+ {foreach from=$actions key=_packageID item=$availableActions}
+ {assign var=_package value=$packages[$_packageID]}
+
+ <optgroup label="{$_package}" data-package-id="{@$_package->packageID}">
+ {foreach from=$availableActions key=actionName item=actionLabel}
+ <option value="{$actionName}"{if $action === $actionName} selected{/if}>{$actionLabel}</option>
+ {/foreach}
+ </optgroup>
+ {/foreach}
+ </select>
+ </dd>
+ </dl>
+
+ <dl class="col-xs-12 col-md-4">
+ <dt></dt>
+ <dd>
+ <input type="date" name="afterDate" id="afterDate" value="{$afterDate}" placeholder="{lang}wcf.acp.modificationLog.time.afterDate{/lang}">
+ </dd>
+ </dl>
+
+ <dl class="col-xs-12 col-md-4">
+ <dt></dt>
+ <dd>
+ <input type="date" name="beforeDate" id="beforeDate" value="{$beforeDate}" placeholder="{lang}wcf.acp.modificationLog.time.beforeDate{/lang}">
+ </dd>
+ </dl>
+
+ {event name='filterFields'}
+ </div>
+
+ <div class="formSubmit">
+ <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s">
+ {@SECURITY_TOKEN_INPUT_TAG}
+ </div>
+ </section>
+</form>
+
+{capture assign=pageParameters}{if $username}&username={$username}{/if}{if $packageID}&packageID={@$packageID}{/if}{if $action}&action={$action}{/if}{if $afterDate}&afterDate={$afterDate}{/if}{if $beforeDate}&beforeDate={$beforeDate}{/if}{/capture}
+{hascontent}
+ <div class="paginationTop">
+ {content}{pages print=true assign=pagesLinks controller="ModificationLogList" link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder$pageParameters"}{/content}
+ </div>
+{/hascontent}
+
+{if $logItems|count}
+ <div class="section tabularBox">
+ <table class="table">
+ <thead>
+ <tr>
+ <th class="columnLogID{if $sortField == 'logID'} active {@$sortOrder}{/if}"><a href="{link controller='ModificationLogList'}pageNo={@$pageNo}&sortField=logID&sortOrder={if $sortField == 'logID' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{$pageParameters}{/link}">{lang}wcf.global.objectID{/lang}</a></th>
+ <th class="columnText columnUsername{if $sortField == 'username'} active {@$sortOrder}{/if}"><a href="{link controller='ModificationLogList'}pageNo={@$pageNo}&sortField=username&sortOrder={if $sortField == 'username' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{$pageParameters}{/link}">{lang}wcf.user.username{/lang}</a></th>
+ <th class="columnText columnAction">{lang}wcf.acp.modificationLog.action{/lang}</th>
+ <th class="columnText columnAffectedObject">{lang}wcf.acp.modificationLog.affectedObject{/lang}</th>
+ <th class="columnDate columnTime{if $sortField == 'time'} active {@$sortOrder}{/if}"><a href="{link controller='ModificationLogList'}pageNo={@$pageNo}&sortField=time&sortOrder={if $sortField == 'time' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{$pageParameters}{/link}">{lang}wcf.global.date{/lang}</a></th>
+
+ {event name='columnHeads'}
+ </tr>
+ </thead>
+
+ <tbody>
+ {foreach from=$logItems item=modificationLog}
+ {assign var=_objectType value=$objectTypes[$modificationLog->objectTypeID]}
+
+ <tr>
+ <td class="columnID columnLogID">{@$modificationLog->logID}</td>
+ <td class="columnText columnUsername">{if $modificationLog->userID}<a href="{link controller='User' id=$modificationLog->userID}{/link}">{$modificationLog->username}</a>{else}{$modificationLog->username}{/if}</td>
+ <td class="columnText columnAction">{lang}wcf.acp.modificationLog.{$_objectType->objectType}.{$modificationLog->action}{/lang}</td>
+ <td class="columnText columnAffectedObject" title="{lang}wcf.acp.modificationLog.affectedObject.id{/lang}">
+ {if $modificationLog->getAffectedObject()}
+ <a href="{$modificationLog->getAffectedObject()->getLink()}">{$modificationLog->getAffectedObject()->getTitle()}</a>
+ {else}
+ <small>{lang}wcf.acp.modificationLog.affectedObject.unknown{/lang}</small>
+ {/if}
+ </td>
+ <td class="columnDate columnTime">{@$modificationLog->time|time}</td>
+
+ {event name='columns'}
+ </tr>
+ {/foreach}
+ </tbody>
+ </table>
+ </div>
+
+ <footer class="contentFooter">
+ {hascontent}
+ <div class="paginationBottom">
+ {content}{@$pagesLinks}{/content}
+ </div>
+ {/hascontent}
+
+ {hascontent}
+ <nav class="contentFooterNavigation">
+ <ul>
+ {content}{event name='contentFooterNavigation'}{/content}
+ </ul>
+ </nav>
+ {/hascontent}
+ </footer>
+{else}
+ <p class="info">{lang}wcf.global.noItems{/lang}</p>
+{/if}
+
+{include file='footer'}
--- /dev/null
+<?php
+declare(strict_types=1);
+namespace wcf\acp\page;
+use wcf\data\modification\log\IViewableModificationLog;
+use wcf\data\modification\log\ModificationLogList;
+use wcf\data\object\type\ObjectType;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\data\package\Package;
+use wcf\page\SortablePage;
+use wcf\system\log\modification\IExtendedModificationLogHandler;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Shows a list of modification log items.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2018 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Acp\Page
+ *
+ * @property ModificationLogList $objectList
+ * @since 3.2
+ */
+class ModificationLogListPage extends SortablePage {
+ /**
+ * filter by action
+ *
+ * @var string
+ */
+ public $action = '';
+
+ /**
+ * list of available actions per package
+ *
+ * @var string[][]
+ */
+ public $actions = [];
+
+ /**
+ * @inheritDoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.log.modification';
+
+ /**
+ * filter by time
+ *
+ * @var string
+ */
+ public $afterDate = '';
+
+ /**
+ * @var int[]
+ */
+ public $availableObjectTypeIDs = [];
+
+ /**
+ * filter by time
+ *
+ * @var string
+ */
+ public $beforeDate = '';
+
+ /**
+ * @inheritDoc
+ */
+ public $defaultSortField = 'time';
+
+ /**
+ * @inheritDoc
+ */
+ public $defaultSortOrder = 'DESC';
+
+ /**
+ * @inheritDoc
+ */
+ public $objectListClassName = ModificationLogList::class;
+
+ /**
+ * @var IViewableModificationLog[]
+ */
+ public $logItems = [];
+
+ /**
+ * @inheritDoc
+ */
+ public $neededPermissions = ['admin.management.canViewLog'];
+
+ /**
+ * @var ObjectType[]
+ */
+ public $objectTypes = [];
+
+ /**
+ * @var Package[]
+ */
+ public $packages = [];
+
+ /**
+ * filter by package id
+ *
+ * @var int
+ */
+ public $packageID = 0;
+
+ /**
+ * list of object types that are not implementing the new API
+ *
+ * @var ObjectType
+ */
+ public $unsupportedObjectTypes = [];
+
+ /**
+ * filter by username
+ *
+ * @var string
+ */
+ public $username = '';
+
+ /**
+ * @inheritDoc
+ */
+ public $validSortFields = [
+ 'logID',
+ 'username',
+ 'time'
+ ];
+
+ /**
+ * @inheritDoc
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ $this->initObjectTypes();
+
+ if (!empty($_REQUEST['action'])) {
+ $this->action = StringUtil::trim($_REQUEST['action']);
+ }
+ if (!empty($_REQUEST['afterDate'])) {
+ $this->afterDate = StringUtil::trim($_REQUEST['afterDate']);
+ }
+ if (!empty($_REQUEST['beforeDate'])) {
+ $this->beforeDate = StringUtil::trim($_REQUEST['beforeDate']);
+ }
+ if (!empty($_REQUEST['packageID'])) {
+ $this->packageID = intval($_REQUEST['packageID']);
+ }
+ if (!empty($_REQUEST['username'])) {
+ $this->username = StringUtil::trim($_REQUEST['username']);
+ }
+ }
+
+ protected function initObjectTypes() {
+ foreach (ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.modifiableContent') as $objectType) {
+ /** @noinspection PhpUndefinedFieldInspection */
+ if ($objectType->excludeFromLogList) {
+ continue;
+ }
+
+ $this->objectTypes[$objectType->objectTypeID] = $objectType;
+
+ /** @var IExtendedModificationLogHandler $processor */
+ $processor = $objectType->getProcessor();
+ if ($processor === null) {
+ $this->unsupportedObjectTypes[] = $objectType;
+ }
+ else {
+ $this->availableObjectTypeIDs[] = $objectType->objectTypeID;
+ if (!isset($this->packages[$objectType->packageID])) {
+ $this->actions[$objectType->packageID] = [];
+ $this->packages[$objectType->packageID] = $objectType->getPackage();
+ }
+
+ foreach ($processor->getAvailableActions() as $action) {
+ $this->actions[$objectType->packageID]["{$objectType->objectType}-{$action}"] = WCF::getLanguage()->get("wcf.acp.modificationLog.{$objectType->objectType}.{$action}");
+ }
+ }
+ }
+
+ foreach ($this->actions as &$actions) {
+ asort($actions, SORT_NATURAL);
+ }
+ unset($actions);
+
+ uasort($this->packages, function (Package $a, Package $b) {
+ return strnatcasecmp($a->package, $b->package);
+ });
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function initObjectList() {
+ parent::initObjectList();
+
+ if (!empty($this->availableObjectTypeIDs)) {
+ $action = '';
+ $objectTypeID = 0;
+ if (preg_match('~^(?P<objectType>.+)\-(?P<action>[^\-]+)$~', $this->action, $matches)) {
+ foreach ($this->objectTypes as $objectType) {
+ if ($objectType->objectType === $matches['objectType']) {
+ /** @var IExtendedModificationLogHandler $processor */
+ $processor = $objectType->getProcessor();
+ if ($processor !== null && in_array($matches['action'], $processor->getAvailableActions())) {
+ $action = $matches['action'];
+ $objectTypeID = $objectType->objectTypeID;
+ }
+
+ break;
+ }
+ }
+ }
+
+ if ($objectTypeID) {
+ $this->objectList->getConditionBuilder()->add('modification_log.objectTypeID = ?', [$objectTypeID]);
+ $this->objectList->getConditionBuilder()->add('modification_log.action = ?', [$action]);
+ }
+ else {
+ if (isset($this->packages[$this->packageID])) {
+ $objectTypeIDs = [];
+ foreach ($this->objectTypes as $objectType) {
+ if ($objectType->packageID == $this->packageID) {
+ $objectTypeIDs[] = $objectType->objectTypeID;
+ }
+ }
+
+ $this->objectList->getConditionBuilder()->add('modification_log.objectTypeID IN (?)', [$objectTypeIDs]);
+ }
+ else {
+ $this->objectList->getConditionBuilder()->add('modification_log.objectTypeID IN (?)', [$this->availableObjectTypeIDs]);
+ }
+ }
+
+ if (!empty($this->username)) {
+ $this->objectList->getConditionBuilder()->add('modification_log.username LIKE ?', [addcslashes($this->username, '%') . '%']);
+ }
+
+ $afterDate = $beforeDate = 0;
+ if (!empty($this->afterDate)) {
+ $afterDate = intval(@strtotime($this->afterDate));
+ }
+ if (!empty($this->beforeDate)) {
+ $beforeDate = intval(@strtotime($this->beforeDate));
+ }
+
+ if ($afterDate && $beforeDate) {
+ $this->objectList->getConditionBuilder()->add('modification_log.time BETWEEN ? AND ?', [
+ $afterDate,
+ $beforeDate
+ ]);
+ }
+ else {
+ if ($afterDate) {
+ $this->objectList->getConditionBuilder()->add('modification_log.time > ?', [$afterDate]);
+ }
+ else if ($beforeDate) {
+ $this->objectList->getConditionBuilder()->add('modification_log.time < ?', [$beforeDate]);
+ }
+ }
+ }
+ else {
+ $this->objectList->getConditionBuilder()->add('1=0');
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readData() {
+ parent::readData();
+
+ $itemsPerType = [];
+ foreach ($this->objectList as $modificationLog) {
+ if (!isset($itemsPerType[$modificationLog->objectTypeID])) {
+ $itemsPerType[$modificationLog->objectTypeID] = [];
+ }
+
+ $itemsPerType[$modificationLog->objectTypeID][] = $modificationLog;
+ }
+
+ if (!empty($itemsPerType)) {
+ foreach ($this->objectTypes as $objectType) {
+ /** @var IExtendedModificationLogHandler $processor */
+ $processor = $objectType->getProcessor();
+ if ($processor === null) {
+ continue;
+ }
+
+ if (isset($itemsPerType[$objectType->objectTypeID])) {
+ $this->logItems = array_merge($this->logItems, $processor->processItems($itemsPerType[$objectType->objectTypeID]));
+ }
+ }
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'action' => $this->action,
+ 'actions' => $this->actions,
+ 'afterDate' => $this->afterDate,
+ 'beforeDate' => $this->beforeDate,
+ 'logItems' => $this->logItems,
+ 'objectTypes' => $this->objectTypes,
+ 'packageID' => $this->packageID,
+ 'packages' => $this->packages,
+ 'unsupportedObjectTypes' => $this->unsupportedObjectTypes,
+ 'username' => $this->username,
+ ]);
+ }
+}
--- /dev/null
+<?php
+declare(strict_types=1);
+namespace wcf\data\modification\log;
+use wcf\data\ITitledLinkObject;
+
+/**
+ * Common interface for modification log handlers that support item processing for
+ * display in the global modification log.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2018 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\System\Log\Modification
+ * @since 3.2
+ */
+interface IViewableModificationLog {
+ /**
+ * Returns the title of the affected object. If the object does not exist
+ * anymore, this method should return an empty string instead. (nullable
+ * requires PHP 7.1)
+ *
+ * @return ITitledLinkObject|null
+ */
+ public function getAffectedObject();
+}
declare(strict_types=1);
namespace wcf\data\object\type;
use wcf\data\object\type\definition\ObjectTypeDefinition;
+use wcf\data\package\Package;
+use wcf\data\package\PackageCache;
use wcf\data\ProcessibleDatabaseObject;
use wcf\data\TDatabaseObjectOptions;
use wcf\data\TDatabaseObjectPermissions;
public function getDefinition() {
return ObjectTypeCache::getInstance()->getDefinition($this->definitionID);
}
+
+ /**
+ * Returns the package that this object type belongs to.
+ *
+ * @return Package
+ * @since 3.2
+ */
+ public function getPackage(): Package {
+ return PackageCache::getInstance()->getPackage($this->packageID);
+ }
}
--- /dev/null
+<?php
+declare(strict_types=1);
+namespace wcf\system\log\modification;
+
+/**
+ * Abstract implementation of a modification log handler that can provide readable outputs
+ * for the global modification log in the ACP.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2018 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\System\Log\Modification
+ * @since 3.2
+ */
+abstract class AbstractExtendedModificationLogHandler extends AbstractModificationLogHandler implements IExtendedModificationLogHandler {
+}
* modifiable content object type
* @var ObjectType
*/
- protected $objectType = null;
+ protected $objectType;
/**
* name of the modifiable content object type
--- /dev/null
+<?php
+declare(strict_types=1);
+namespace wcf\system\log\modification;
+use wcf\data\modification\log\IViewableModificationLog;
+use wcf\data\modification\log\ModificationLog;
+
+/**
+ * Common interface for modification log handlers that support item processing for
+ * display in the global modification log.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2018 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\System\Log\Modification
+ * @since 3.2
+ */
+interface IExtendedModificationLogHandler {
+ /**
+ * Returns the list of possible actions for this object type.
+ *
+ * @return string[]
+ */
+ public function getAvailableActions(): array;
+
+ /**
+ * Processes a list of items by converting them into IViewableModificationLog
+ * instances and pre-loading their data.
+ *
+ * @param ModificationLog[] $items
+ * @return IViewableModificationLog[]
+ */
+ public function processItems(array $items): array;
+}
<item name="wcf.acp.menu.link.devtools.project.add"><![CDATA[Projekt hinzufügen]]></item>
<item name="wcf.acp.menu.link.devtools.project.list"><![CDATA[Projekte]]></item>
<item name="wcf.acp.menu.link.devtools.notificationTest"><![CDATA[Benachrichtigungstest]]></item>
+ <item name="wcf.acp.menu.link.log.modification"><![CDATA[Globales Veränderungsprotokoll]]></item>
+ </category>
+
+ <category name="wcf.acp.modificationLog">
+ <item name="wcf.acp.modificationLog.list"><![CDATA[Globales Veränderungsprotokoll]]></item>
+ <item name="wcf.acp.modificationLog.action"><![CDATA[Aktion]]></item>
+ <item name="wcf.acp.modificationLog.action.all"><![CDATA[Alle Aktion]]></item>
+ <item name="wcf.acp.modificationLog.affectedObject"><![CDATA[Betroffenes Objekt]]></item>
+ <item name="wcf.acp.modificationLog.affectedObject.id"><![CDATA[ID: {@$modificationLog->objectID}]]></item>
+ <item name="wcf.acp.modificationLog.affectedObject.unknown"><![CDATA[(Existiert nicht mehr)]]></item>
+ <item name="wcf.acp.modificationLog.package.all"><![CDATA[Alle Quellen]]></item>
+ <item name="wcf.acp.modificationLog.time.afterDate"><![CDATA[Ab dem Zeitpunkt]]></item>
+ <item name="wcf.acp.modificationLog.time.beforeDate"><![CDATA[Vor dem Zeitpunkt]]></item>
+ <item name="wcf.acp.modificationLog.unsupportedObjectTypes"><![CDATA[Einige Typen werden aktuell nicht unterstützt und müssen zuvor vom Hersteller angepasst werden.]]></item>
</category>
<category name="wcf.acp.notice">
<item name="wcf.acp.menu.link.devtools.project.add"><![CDATA[Add Project]]></item>
<item name="wcf.acp.menu.link.devtools.project.list"><![CDATA[Projects]]></item>
<item name="wcf.acp.menu.link.devtools.notificationTest"><![CDATA[Notification Test]]></item>
+ <item name="wcf.acp.menu.link.log.modification"><![CDATA[Global Modification Log]]></item>
+ </category>
+
+ <category name="wcf.acp.modificationLog">
+ <item name="wcf.acp.modificationLog.list"><![CDATA[Global Modification Log]]></item>
+ <item name="wcf.acp.modificationLog.action"><![CDATA[Action]]></item>
+ <item name="wcf.acp.modificationLog.action.all"><![CDATA[All Actions]]></item>
+ <item name="wcf.acp.modificationLog.affectedObject"><![CDATA[Affected Object]]></item>
+ <item name="wcf.acp.modificationLog.affectedObject.id"><![CDATA[ID: {@$modificationLog->objectID}]]></item>
+ <item name="wcf.acp.modificationLog.affectedObject.unknown"><![CDATA[(Does not exist anymore)]]></item>
+ <item name="wcf.acp.modificationLog.package.all"><![CDATA[All Sources]]></item>
+ <item name="wcf.acp.modificationLog.time.afterDate"><![CDATA[After the date]]></item>
+ <item name="wcf.acp.modificationLog.time.beforeDate"><![CDATA[Before the date]]></item>
+ <item name="wcf.acp.modificationLog.unsupportedObjectTypes"><![CDATA[Some types are currently not supported and require changes to be made by the vendor.]]></item>
</category>
<category name="wcf.acp.notice">