[FEATURE] Rewrite backend module 62/48862/6
authorFrancois Suter <francois@typo3.org>
Sat, 9 Jul 2016 09:23:45 +0000 (11:23 +0200)
committerFrancois Suter <francois@typo3.org>
Sat, 24 Sep 2016 20:48:49 +0000 (22:48 +0200)
New backend module, based on TYPO3 7 best practices.

Change-Id: Ice0f771cb630e9bc7309cdaf97be280f0a55073f
Resolves: #76970
Releases: 3.0
Reviewed-on: https://review.typo3.org/48862
Reviewed-by: Francois Suter <francois@typo3.org>
Tested-by: Francois Suter <francois@typo3.org>
19 files changed:
ChangeLog
Classes/Controller/ListModuleController.php [new file with mode: 0644]
Classes/Domain/Model/ExtensionConfiguration.php
Classes/Domain/Repository/EntryRepository.php
Classes/Template/Components/Buttons/ExtendedLinkButton.php [new file with mode: 0644]
Configuration/Backend/AjaxRoutes.php [new file with mode: 0644]
Resources/Private/Language/Module.xlf [new file with mode: 0644]
Resources/Private/Language/locallang.xlf
Resources/Private/Templates/ListModule/Index.html [new file with mode: 0644]
Resources/Public/Images/ModuleIcon.svg [new file with mode: 0644]
Resources/Public/JavaScript/ListModule.js [new file with mode: 0644]
Resources/Public/JavaScript/datatables.mark.min.js [new file with mode: 0644]
Resources/Public/JavaScript/jquery.mark.min.js [new file with mode: 0644]
Resources/Public/StyleSheet/Devlog.css [new file with mode: 0644]
Tests/Functional/Domain/Repository/EntryRepositoryTest.php [new file with mode: 0644]
Tests/Functional/Domain/Repository/Fixtures/DevlogEntries.xml [new file with mode: 0644]
ext_conf_template.txt
ext_localconf.php
ext_tables.php

index 5e2d91c..4b2ac57 100755 (executable)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,11 @@
+2016-07-09 Francois Suter <typo3@cobweb.ch>
+
+       * New backend module, resolves #76970
+
+2016-07-06 Stefan Froemken <froemken@gmail.com>
+
+       * Move extension configuration to domain model, resolves #73691
+
 2014-12-24 Francois Suter <typo3@cobweb.ch>
 
        * Add sorting field, resolves #64037
diff --git a/Classes/Controller/ListModuleController.php b/Classes/Controller/ListModuleController.php
new file mode 100644 (file)
index 0000000..1b6d922
--- /dev/null
@@ -0,0 +1,186 @@
+<?php
+namespace Devlog\Devlog\Controller;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Devlog\Devlog\Domain\Model\ExtensionConfiguration;
+use Devlog\Devlog\Domain\Repository\EntryRepository;
+use Devlog\Devlog\Template\Components\Buttons\ExtendedLinkButton;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Backend\Template\Components\ButtonBar;
+use TYPO3\CMS\Backend\View\BackendTemplateView;
+use TYPO3\CMS\Core\Imaging\Icon;
+use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
+use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
+use TYPO3\CMS\Extbase\Object\ObjectManager;
+use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
+
+/**
+ * Controller for the "Developer's Log" backend module
+ *
+ * @author Francois Suter (Cobweb) <typo3@cobweb.ch>
+ * @package TYPO3
+ * @subpackage tx_devlog
+ */
+class ListModuleController extends ActionController
+{
+
+    /**
+     * @var BackendTemplateView
+     */
+    protected $view;
+
+    /**
+     * @var EntryRepository
+     */
+    protected $entryRepository;
+
+    /**
+     * Devlog extension configuration
+     *
+     * @var ExtensionConfiguration
+     */
+    protected $extensionConfiguration = null;
+
+    /**
+     * Injects an instance of the entry repository.
+     *
+     * @param EntryRepository $entryRepository
+     * @return void
+     */
+    public function injectLogRepository(EntryRepository $entryRepository)
+    {
+        $this->entryRepository = $entryRepository;
+    }
+
+    /**
+     * @param ExtensionConfiguration $extensionConfiguration
+     */
+    public function injectExtensionConfiguration(ExtensionConfiguration $extensionConfiguration)
+    {
+        $this->extensionConfiguration = $extensionConfiguration;
+    }
+
+    /**
+     * Initializes the template to use for all actions.
+     *
+     * @return void
+     */
+    protected function initializeAction()
+    {
+        $this->defaultViewObjectName = BackendTemplateView::class;
+    }
+
+    /**
+     * Performs initializations of certain objects during calls in an AJAX context.
+     *
+     * In this particular context, the Extbase bootstrapping does not occur.
+     * Some objects must be instantiated "manually".
+     *
+     * @return void
+     */
+    protected function initializeForAjaxAction()
+    {
+        $this->objectManager = GeneralUtility::makeInstance(ObjectManager::class);
+        $this->entryRepository = $this->objectManager->get(EntryRepository::class);
+    }
+
+    /**
+     * Initializes the view before invoking an action method.
+     *
+     * @param ViewInterface $view The view to be initialized
+     * @return void
+     * @api
+     */
+    protected function initializeView(ViewInterface $view)
+    {
+        if ($view instanceof BackendTemplateView) {
+            parent::initializeView($view);
+        }
+        $pageRenderer = $view->getModuleTemplate()->getPageRenderer();
+        $pageRenderer->addCssFile(
+                ExtensionManagementUtility::extRelPath('devlog') . 'Resources/Public/StyleSheet/Devlog.css'
+        );
+        $pageRenderer->loadRequireJsModule('TYPO3/CMS/Devlog/ListModule');
+        $pageRenderer->addInlineSettingArray(
+                'DevLog',
+                $this->extensionConfiguration->toArray()
+        );
+        // Add open in new window button
+        $newWindowIcon = $this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-window-open', Icon::SIZE_SMALL);
+        $newWindowButton = GeneralUtility::makeInstance(ExtendedLinkButton::class);
+        $newWindowButton->setIcon($newWindowIcon)
+                ->setTarget('_blank')
+                ->setTitle(LocalizationUtility::translate('LLL:EXT:lang/locallang_core.xlf:labels.openInNewWindow', 'lang'))
+                ->setHref(
+                        $this->uriBuilder->uriFor('index')
+                );
+        $this->view->getModuleTemplate()->getDocHeaderComponent()->getButtonBar()->addButton(
+                $newWindowButton,
+                ButtonBar::BUTTON_POSITION_RIGHT
+        );
+    }
+
+    /**
+     * Displays the list of all available log entries.
+     *
+     * @return void
+     */
+    public function indexAction()
+    {
+
+    }
+
+    /**
+     * Returns the list of all log entries, in JSON format.
+     *
+     * @param ServerRequestInterface $request
+     * @param ResponseInterface $response
+     * @return ResponseInterface
+     */
+    public function getAllAction(ServerRequestInterface $request, ResponseInterface $response)
+    {
+        $this->initializeForAjaxAction();
+
+        // Get all the entries and make them into an array for JSON encoding
+        $entries = $this->entryRepository->findAll();
+        // Send the response
+        $response->getBody()->write(json_encode($entries));
+        return $response;
+    }
+
+    /**
+     * Returns the list of all log entries after a given timestamp.
+     *
+     * @param ServerRequestInterface $request
+     * @param ResponseInterface $response
+     * @return ResponseInterface
+     */
+    public function getNewAction(ServerRequestInterface $request, ResponseInterface $response)
+    {
+        $this->initializeForAjaxAction();
+        $requestParameters = $request->getQueryParams();
+
+        // Get all the entries and make them into an array for JSON encoding
+        $entries = $this->entryRepository->findAfterDate(
+                $requestParameters['timestamp']
+        );
+        // Send the response
+        $response->getBody()->write(json_encode($entries));
+        return $response;
+    }
+}
\ No newline at end of file
index dfc73a2..92a13e9 100644 (file)
@@ -21,12 +21,19 @@ use TYPO3\CMS\Core\SingletonInterface;
  * Object containing the extension configuration.
  *
  * NOTE: this is not a true Extbase object.
- * 
+ *
  * @author Stefan Froemken <froemken@gmail.com>
  */
 class ExtensionConfiguration implements SingletonInterface
 {
     /**
+     * Raw configuration
+     *
+     * @var array
+     */
+    protected $configurationArray = array();
+
+    /**
      * Minimum log level
      *
      * @var int
@@ -97,10 +104,10 @@ class ExtensionConfiguration implements SingletonInterface
     public function __construct()
     {
         // Get global configuration
-        $extensionConfiguration = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['devlog']);
-        if (is_array($extensionConfiguration)) {
+        $this->configurationArray = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['devlog']);
+        if (is_array($this->configurationArray)) {
             // Call setter method foreach configuration entry
-            foreach ($extensionConfiguration as $key => $value) {
+            foreach ($this->configurationArray as $key => $value) {
                 $methodName = 'set' . ucfirst($key);
                 if (method_exists($this, $methodName)) {
                     $this->$methodName($value);
@@ -110,6 +117,16 @@ class ExtensionConfiguration implements SingletonInterface
     }
 
     /**
+     * Returns the extension configuration as array.
+     *
+     * @return array
+     */
+    public function toArray()
+    {
+        return $this->configurationArray;
+    }
+
+    /**
      * Returns the minimumLogLevel.
      *
      * @return int $minimumLogLevel
index 1dae7b6..bc0ca34 100644 (file)
@@ -15,6 +15,7 @@ namespace Devlog\Devlog\Domain\Repository;
  */
 
 use Devlog\Devlog\Domain\Model\ExtensionConfiguration;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\SingletonInterface;
 
 /**
@@ -60,14 +61,34 @@ class EntryRepository implements SingletonInterface
                     '',
                     'crdate DESC, sorting ASC'
             );
-        }
-        catch (\Exception $e) {
+        } catch (\Exception $e) {
             $entries = array();
         }
-        $numEntries = count($entries);
-        for ($i = 0; $i < $numEntries; $i++) {
-            $entries[$i]['extra_data'] = gzuncompress($entries[$i]['extra_data']);
+        $entries = $this->expandEntryData($entries);
+        return $entries;
+    }
+
+    /**
+     * Finds all entries at or after the given timestamp.
+     *
+     * @param int $timestamp Limit date/time for fetching entries
+     * @return array
+     */
+    public function findAfterDate($timestamp)
+    {
+        try {
+            $entries = $this->getDatabaseConnection()->exec_SELECTgetRows(
+                    '*',
+                    $this->databaseTable,
+                    'crdate >= ' . (int)$timestamp,
+                    '',
+                    'crdate DESC, sorting ASC'
+            );
+        } catch (\Exception $e) {
+            $entries = array();
         }
+        $entries = $this->expandEntryData($entries);
+        return $entries;
     }
 
     /**
@@ -92,13 +113,20 @@ class EntryRepository implements SingletonInterface
                 'pid' => $entry->getPid(),
         );
         // Handle extra data
-        $fields['extra_data'] = gzcompress(serialize($entry->getExtraData()));
-        $extraDataSize = strlen($fields['extra_data']);
-        $maximumExtraDataSize = $this->extensionConfiguration->getMaximumExtraDataSize();
-        // If the entry's extra data is above the limit, replace it with a warning
-        if (!empty($maximumExtraDataSize) && $extraDataSize > $maximumExtraDataSize) {
-            $fields['extra_data'] = gzcompress(serialize('Extra data too large, not saved.'));
+        $extraData = $entry->getExtraData();
+        // NOTE: GeneralUtility::devLog() sends "false" if extra data is undefined
+        if ($extraData) {
+            $fields['extra_data'] = gzcompress(serialize($extraData));
+            $extraDataSize = strlen($fields['extra_data']);
+            $maximumExtraDataSize = $this->extensionConfiguration->getMaximumExtraDataSize();
+            // If the entry's extra data is above the limit, replace it with a warning
+            if (!empty($maximumExtraDataSize) && $extraDataSize > $maximumExtraDataSize) {
+                $fields['extra_data'] = gzcompress(serialize('Extra data too large, not saved.'));
+            }
+        } else {
+            $fields['extra_data'] = '';
         }
+
         return $this->getDatabaseConnection()->exec_INSERTquery(
                 $this->databaseTable,
                 $fields
@@ -154,6 +182,83 @@ class EntryRepository implements SingletonInterface
     }
 
     /**
+     * Collects additional data or transforms data from entries for simpler handling during display.
+     *
+     * @param array $entries
+     * @return array
+     */
+    protected function expandEntryData(array $entries)
+    {
+        $pageInformationCache = array();
+        $numEntries = count($entries);
+        if ($numEntries > 0) {
+            $users = $this->findAllUsers();
+            for ($i = 0; $i < $numEntries; $i++) {
+                // Grab username instead of id
+                $userId = (int)$entries[$i]['cruser_id'];
+                if ($userId > 0 && isset($users[$userId])) {
+                    $entries[$i]['username'] = $users[$userId]['username'];
+                } else {
+                    $entries[$i]['username'] = '';
+                }
+                // Grab page title
+                $pid = (int)$entries[$i]['pid'];
+                if ($pid > 0 && isset($pageInformationCache[$pid])) {
+                    $entries[$i]['page'] = $pageInformationCache[$pid];
+                } else {
+                    $pageTitle = $pid;
+                    $pageRecord = BackendUtility::getRecord(
+                            'pages',
+                            $pid
+                    );
+                    if (is_array($pageRecord)) {
+                        $title = BackendUtility::getRecordTitle(
+                                'pages',
+                                $pageRecord
+                        );
+                        $pageTitle = $title . ' [' . $pid . ']';
+                    }
+                    $entries[$i]['page'] = $pageTitle;
+                    $pageInformationCache[$pid] = $pageTitle;
+                }
+                // Process extra data (uncompress and dump)
+                if ($entries[$i]['extra_data'] === '') {
+                    $extraData = '';
+                } else {
+                    $extraData = gzuncompress($entries[$i]['extra_data']);
+                    $extraData = var_export(unserialize($extraData), true);
+                }
+                $entries[$i]['extra_data'] = $extraData;
+            }
+            unset($pageInformationCache);
+        }
+        return $entries;
+    }
+
+    /**
+     * Fetches the list of all BE users.
+     *
+     * @return array
+     */
+    protected function findAllUsers()
+    {
+        try {
+            $users = $this->getDatabaseConnection()->exec_SELECTgetRows(
+                    'uid, username',
+                    'be_users',
+                    '',
+                    '',
+                    '',
+                    '',
+                    'uid'
+            );
+        } catch (\Exception $e) {
+            $users = array();
+        }
+        return $users;
+    }
+
+    /**
      * Sets the extension configuration.
      *
      * Used to pass the "devlog" configuration down to the entry repository.
diff --git a/Classes/Template/Components/Buttons/ExtendedLinkButton.php b/Classes/Template/Components/Buttons/ExtendedLinkButton.php
new file mode 100644 (file)
index 0000000..7138482
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+namespace Devlog\Devlog\Template\Components\Buttons;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Backend\Template\Components\Buttons\LinkButton;
+
+/**
+ * Extends the \TYPO3\CMS\Backend\Template\Components\Buttons\LinkButton class
+ * with a "target" attribute for the button link.
+ *
+ * @package Devlog\Devlog\Template\Components\Buttons
+ */
+class ExtendedLinkButton extends LinkButton
+{
+    /**
+     * @var string Target for the button hyperlink
+     */
+    protected $target = '';
+
+    /**
+     * Sets the value of the target attribute.
+     *
+     * @param $target
+     * @return ExtendedLinkButton
+     */
+    public function setTarget($target)
+    {
+        $this->target = $target;
+        return $this;
+    }
+
+    /**
+     * Returns the value of the target attribute.
+     *
+     * @return string
+     */
+    public function getTarget()
+    {
+        return $this->target;
+    }
+
+    /**
+     * Overrides the parent render method to insert the target attribute in the button link.
+     *
+     * @return string
+     */
+    public function render()
+    {
+        // Get the rendered button and insert the target attribute
+        $button = parent::render();
+        if ($this->target !== '') {
+            $button = preg_replace('/(href=".*?")/', '$1 target="' . $this->target .'"', $button);
+        }
+        return $button;
+    }
+
+    /**
+     * Validates the current button.
+     *
+     * Since a class check is included, we need to override the parent class validation.
+     *
+     * @return bool
+     */
+    public function isValid()
+    {
+        if (
+            trim($this->getHref()) !== ''
+            && trim($this->getTitle()) !== ''
+            && $this->getType() === ExtendedLinkButton::class
+            && $this->getIcon() !== null
+        ) {
+            return true;
+        }
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/Configuration/Backend/AjaxRoutes.php b/Configuration/Backend/AjaxRoutes.php
new file mode 100644 (file)
index 0000000..d43d15b
--- /dev/null
@@ -0,0 +1,12 @@
+<?php
+
+return [
+        'tx_devlog_list' => [
+                'path' => '/devlog/list/get',
+                'target' => \Devlog\Devlog\Controller\ListModuleController::class . '::getAllAction'
+        ],
+        'tx_devlog_reload' => [
+                'path' => '/devlog/list/reload',
+                'target' => \Devlog\Devlog\Controller\ListModuleController::class . '::getNewAction'
+        ]
+];
diff --git a/Resources/Private/Language/Module.xlf b/Resources/Private/Language/Module.xlf
new file mode 100644 (file)
index 0000000..a290529
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+       <file original="" product-name="Devlog.Devlog" source-language="en" datatype="plaintext">
+               <body>
+                       <trans-unit id="mlang_tabs_tab" xml:space="preserve">
+                               <source>Developer's Log</source>
+                       </trans-unit>
+                       <trans-unit id="mlang_labels_tablabel" xml:space="preserve">
+                               <source>Backend module for viewing devlog entries</source>
+                       </trans-unit>
+                       <trans-unit id="mlang_labels_tabdescr" xml:space="preserve">
+                               <source>View and filter devlog entries made to the database. Entries made elsewhere need to be browsed with other tools.</source>
+                       </trans-unit>
+               </body>
+       </file>
+</xliff>
index c6ab014..44ac51f 100644 (file)
@@ -4,7 +4,7 @@
                <header/>
                <body>
                        <trans-unit id="title" xml:space="preserve">
-                               <source>Developer Log</source>
+                               <source>Developer's Log</source>
                        </trans-unit>
                        <trans-unit id="showlog" xml:space="preserve">
                                <source>Show Log</source>
                        <trans-unit id="all_entries" xml:space="preserve">
                                <source>all entries</source>
                        </trans-unit>
-                       <trans-unit id="auto_refresh" xml:space="preserve">
-                               <source>Auto reload when new data</source>
+                       <trans-unit id="reload" xml:space="preserve">
+                               <source>Reload</source>
+                       </trans-unit>
+                       <trans-unit id="autoReload" xml:space="preserve">
+                               <source>Automatic reload</source>
                        </trans-unit>
                        <trans-unit id="log_time" xml:space="preserve">
                                <source>Log time</source>
                        <trans-unit id="refresh" xml:space="preserve">
                                <source>Refresh</source>
                        </trans-unit>
-                       <trans-unit id="crdate" xml:space="preserve">
+                       <trans-unit id="date" xml:space="preserve">
                                <source>Date</source>
                        </trans-unit>
                        <trans-unit id="severity" xml:space="preserve">
                                <source>Severity</source>
                        </trans-unit>
-                       <trans-unit id="extkey" xml:space="preserve">
-                               <source>Extension</source>
+                       <trans-unit id="extensionKey" xml:space="preserve">
+                               <source>Key (extension, class, ...)</source>
                        </trans-unit>
-                       <trans-unit id="msg" xml:space="preserve">
+                       <trans-unit id="message" xml:space="preserve">
                                <source>Message</source>
                        </trans-unit>
                        <trans-unit id="location" xml:space="preserve">
                                <source>Called from</source>
                        </trans-unit>
-                       <trans-unit id="pid" xml:space="preserve">
+                       <trans-unit id="ipAddress" xml:space="preserve">
+                               <source>IP address</source>
+                       </trans-unit>
+                       <trans-unit id="page" xml:space="preserve">
                                <source>Page</source>
                        </trans-unit>
-                       <trans-unit id="cruser_id" xml:space="preserve">
+                       <trans-unit id="user" xml:space="preserve">
                                <source>User</source>
                        </trans-unit>
-                       <trans-unit id="data_var" xml:space="preserve">
+                       <trans-unit id="extraData" xml:space="preserve">
                                <source>Extra data</source>
                        </trans-unit>
                        <trans-unit id="no_entries_found" xml:space="preserve">
diff --git a/Resources/Private/Templates/ListModule/Index.html b/Resources/Private/Templates/ListModule/Index.html
new file mode 100644 (file)
index 0000000..28fa794
--- /dev/null
@@ -0,0 +1,73 @@
+<html data-namespace-typo3-fluid="true"
+         xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
+         xmlns:core="http://typo3.org/ns/TYPO3/CMS/Core/ViewHelpers">
+       <f:flashMessages />
+
+       <h1><f:translate id="title" /></h1>
+
+       <div id="tx_devlog_list_wrapper" class="hidden">
+               <form name="reload" class="form-inline">
+                       <div class="form-row">
+                               <div class="form-group">
+                                       <button type="button" name="reload" id="tx_devlog_reload" class="btn btn-default">
+                                               <f:translate id="reload" />
+                                       </button>
+                               </div>
+                               <div class="form-group">
+                                       <div class="checkbox">
+                                               <label>
+                                                       <input type="checkbox" name="autoreload" id="tx_devlog_autoreload" value="autoreload">
+                                                       <f:translate id="autoReload" />
+                                               </label>
+                                       </div>
+                               </div>
+                       </div>
+               </form>
+               <div class="clearfix"></div>
+               <form name="searchAndFilter">
+                       <div class="form-group">
+                               <f:form.textfield name="devlog-searchfield" placeholder="{f:translate(key:'search')}"
+                                                                 id="tx_devlog_search"
+                                                                 class="form-control t3js-devlog-searchfield"/>
+                       </div>
+               </form>
+               <table class="table table-striped table-hover" id="tx_devlog_list">
+                       <thead>
+                               <tr>
+                                       <th class="entry-date">
+                                               <f:translate id="date" />
+                                       </th>
+                                       <th class="entry-severity">
+                                               <f:translate id="severity" />
+                                       </th>
+                                       <th class="entry-extension">
+                                               <f:translate id="extensionKey" />
+                                       </th>
+                                       <th class="entry-message">
+                                               <f:translate id="message" />
+                                       </th>
+                                       <th class="entry-location">
+                                               <f:translate id="location" />
+                                       </th>
+                                       <th class="entry-ip">
+                                               <f:translate id="ipAddress" />
+                                       </th>
+                                       <th class="entry-page">
+                                               <f:translate id="page" />
+                                       </th>
+                                       <th class="entry-user">
+                                               <f:translate id="user" />
+                                       </th>
+                                       <th class="entry-data" id="tx_devlog_expand_all">
+                                               <f:translate id="extraData" />
+                                               <span id="tx_devlog_expand_all_icon"><core:icon identifier="actions-view-list-expand" /></span>
+                                       </th>
+                               </tr>
+                       </thead>
+               </table>
+       </div>
+       <!-- Loading mask -->
+       <div id="tx_devlog_list_loader">
+               <core:icon identifier="provider-fontawesome-spinner" size="default" />
+       </div>
+</html>
\ No newline at end of file
diff --git a/Resources/Public/Images/ModuleIcon.svg b/Resources/Public/Images/ModuleIcon.svg
new file mode 100644 (file)
index 0000000..f51d3ac
--- /dev/null
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->\r
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
+       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
+]>\r
+<svg version="1.1"\r
+        xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"\r
+        x="0px" y="0px" width="216px" height="216px" viewBox="0 0.5 216 216" overflow="visible" enable-background="new 0 0.5 216 216"\r
+        xml:space="preserve">\r
+<defs>\r
+</defs>\r
+<rect id="svg_1_1_" fill="#E84141" width="216.377" height="216.596"/>\r
+<path id="svg_5_1_" marker-mid="" marker-end="" marker-start="" fill="none" stroke="#FFFFFF" stroke-width="3" stroke-linejoin="round" d="\r
+       M164.246,20.672H59.153c-6.434,0-11.671,5.032-12.325,11.458h9.989c6.863,0,12.456,5.742,12.456,12.788s-5.593,12.788-12.456,12.788\r
+       h-10.12v6.388h10.12c6.875,0,12.468,5.741,12.468,12.8c0,7.059-5.592,12.794-12.468,12.794h-10.12v6.375h10.12\r
+       c6.863,0,12.456,5.742,12.456,12.788c0,7.059-5.593,12.813-12.456,12.813h-10.12v6.355h10.12c6.863,0,12.456,5.742,12.456,12.801\r
+       s-5.593,12.801-12.456,12.801h-10.12v6.374h10.114c6.875,0,12.468,5.735,12.468,12.794s-5.592,12.801-12.468,12.801H46.723\r
+       c0.143,6.931,5.642,12.513,12.431,12.513h105.093c6.875,0,12.455-5.729,12.455-12.788V33.46\r
+       C176.701,26.401,171.121,20.672,164.246,20.672z M166.581,65.429c0,3.529-2.784,6.394-6.228,6.394H96.52\r
+       c-3.444,0-6.228-2.865-6.228-6.394V37.456c0-3.529,2.784-6.394,6.228-6.394h63.834c3.443,0,6.228,2.865,6.228,6.394V65.429z\r
+        M63.045,172.787c0-3.542-2.796-6.4-6.228-6.4h-20.24c-3.438,0-6.228,2.858-6.228,6.4c0,3.529,2.796,6.395,6.228,6.395h20.24\r
+       C60.249,179.182,63.045,176.316,63.045,172.787z M36.578,51.305h20.247c3.431,0,6.221-2.864,6.221-6.387\r
+       c0-3.548-2.79-6.394-6.221-6.394H36.578c-3.444,0-6.228,2.846-6.228,6.394C30.344,48.44,33.134,51.305,36.578,51.305z M36.578,83.28\r
+       h20.24c3.444,0,6.228-2.864,6.228-6.394s-2.79-6.394-6.228-6.394h-20.24c-3.444,0-6.228,2.865-6.228,6.394\r
+       S33.134,83.28,36.578,83.28z M36.578,115.25h20.24c3.431,0,6.228-2.865,6.228-6.401s-2.79-6.387-6.228-6.387h-20.24\r
+       c-3.444,0-6.234,2.852-6.234,6.387S33.134,115.25,36.578,115.25z M30.35,140.812c0,3.535,2.777,6.406,6.228,6.406h20.24\r
+       c3.431,0,6.228-2.871,6.228-6.406c0-3.529-2.79-6.395-6.228-6.395h-20.24C33.127,134.418,30.35,137.283,30.35,140.812z"/>\r
+<text transform="matrix(0.9569 0 0 1 95.6929 57.0659)" fill="#FFFFFF" font-family="'MyriadPro-Regular'" font-size="23.8886">devlog</text>\r
+</svg>\r
diff --git a/Resources/Public/JavaScript/ListModule.js b/Resources/Public/JavaScript/ListModule.js
new file mode 100644 (file)
index 0000000..afe1fa8
--- /dev/null
@@ -0,0 +1,329 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Module: TYPO3/CMS/Devlog/ListModule
+ * Devlog "List" module JS
+ */
+
+// The "mark.js" plugin for DataTables expects DataTables to be loaded as "datatables.net" when used as a
+// module. Since the TYPO3 Core uses "datatables", create an alias called "datatables.net".
+require.config({
+       map: {
+               '*': {
+                       'datatables.net': 'datatables'
+               }
+       }
+});
+
+define(['jquery',
+               'moment',
+               'TYPO3/CMS/Backend/Icons',
+               'datatables.net',
+               'TYPO3/CMS/Backend/jquery.clearable',
+               './jquery.mark.min',
+               './datatables.mark.min'
+          ], function($, moment, Icons) {
+       'use strict';
+
+       var DevlogListModule = {
+               table: null,
+               severityIcons: {},
+               expandIcon: null,
+               collapseIcon: null,
+               listWrapper: null,
+               loadingMask: null,
+               lastUpdateTime: null,
+               intervalID: 0
+       };
+
+       DevlogListModule.init = function() {
+               this.loadingMask = $('#tx_devlog_list_loader');
+               this.listWrapper = $('#tx_devlog_list_wrapper');
+       };
+
+       /**
+        * Preloads all necessary icons.
+        */
+       DevlogListModule.loadIcons = function() {
+               // Severity icons
+               Icons.getIcon('status-dialog-ok', Icons.sizes.small, '', '').done(function(markup) {
+                       DevlogListModule.severityIcons[-1] = markup;
+               });
+               Icons.getIcon('status-dialog-information', Icons.sizes.small, '', '').done(function(markup) {
+                       DevlogListModule.severityIcons[0] = markup;
+               });
+               Icons.getIcon('status-dialog-notification', Icons.sizes.small, '', '').done(function(markup) {
+                       DevlogListModule.severityIcons[1] = markup;
+               });
+               Icons.getIcon('status-dialog-warning', Icons.sizes.small, '', '').done(function(markup) {
+                       DevlogListModule.severityIcons[2] = markup;
+               });
+               Icons.getIcon('status-dialog-error', Icons.sizes.small, '', '').done(function(markup) {
+                       DevlogListModule.severityIcons[3] = markup;
+               });
+               // Expand and collapse
+               Icons.getIcon('actions-view-list-expand', Icons.sizes.small, '', '').done(function(markup) {
+                       DevlogListModule.expandIcon = markup;
+               });
+               Icons.getIcon('actions-view-list-collapse', Icons.sizes.small, '', '').done(function(markup) {
+                       DevlogListModule.collapseIcon = markup;
+               });
+       };
+
+       /**
+        * Loads log data dynamically and initializes DataTables.
+        *
+        * @param tableView
+        */
+       DevlogListModule.buildDynamicTable = function(tableView) {
+               this.lastUpdateTime = moment().unix();
+               $.ajax({
+                       url: TYPO3.settings.ajaxUrls['tx_devlog_list'],
+                       success: function (data, status, xhr) {
+                               DevlogListModule.table = tableView.DataTable({
+                                       data: data,
+                                       dom: 'tp',
+                                       // Default ordering is "crdate" column
+                                       order: [
+                                               [0, 'desc']
+                                       ],
+                                       mark: true,
+                                       columnDefs: [
+                                               {
+                                                       targets: 'entry-date',
+                                                       data: 'crdate',
+                                                       render:  function(data, type, row, meta) {
+                                                               if (type === 'display' || type === 'filter') {
+                                                                       var date = moment.unix(data);
+                                                                       return date.format('YYYY-MM-DD HH:mm:ss');
+                                                               } else {
+                                                                       // Add timestamp and sorting to get fine ordering (making sure we have integers)
+                                                                       return parseInt(data * 1000) + parseInt(row.sorting);
+                                                               }
+                                                       }
+                                               },
+                                               {
+                                                       targets: 'entry-severity',
+                                                       data: 'severity',
+                                                       render:  function(data, type, row, meta) {
+                                                               if (type === 'display') {
+                                                                       return DevlogListModule.severityIcons[data];
+                                                               } else {
+                                                                       return data;
+                                                               }
+                                                       }
+                                               },
+                                               {
+                                                       targets: 'entry-extension',
+                                                       data: 'extkey'
+                                               },
+                                               {
+                                                       targets: 'entry-message',
+                                                       data: 'message'
+                                               },
+                                               {
+                                                       targets: 'entry-location',
+                                                       data: 'location',
+                                                       render:  function(data, type, row, meta) {
+                                                               return data + ', line ' + row.line;
+                                                       }
+                                               },
+                                               {
+                                                       targets: 'entry-ip',
+                                                       data: 'ip'
+                                               },
+                                               {
+                                                       targets: 'entry-page',
+                                                       data: 'page'
+                                               },
+                                               {
+                                                       targets: 'entry-user',
+                                                       data: 'username'
+                                               },
+                                               {
+                                                       targets: 'entry-data',
+                                                       data: 'extra_data',
+                                                       orderable: false,
+                                                       render:  function(data, type, row, meta) {
+                                                               if (type === 'display') {
+                                                                       var html = '';
+                                                                       if (data !== '') {
+                                                                               html += '<button class="btn btn-default extra-data-toggle">';
+                                                                               html += DevlogListModule.expandIcon;
+                                                                               html += '</button>';
+                                                                               html += '<div class="extra-data-wrapper" style="display: none">';
+                                                                               html += '<pre>' + data + '</pre>';
+                                                                               html += '</div>';
+                                                                       }
+                                                                       return html;
+                                                               } else {
+                                                                       return data;
+                                                               }
+                                                       }
+                                               }
+                                       ],
+                                       initComplete: function() {
+                                               DevlogListModule.initializeSearchField();
+                                               DevlogListModule.initializeExtraDataToggle(tableView);
+                                               DevlogListModule.initializeReloadControls();
+                                               DevlogListModule.toggleLoadingMask();
+                                       }
+                               });
+                       }
+               });
+       };
+
+       /**
+        * Initializes the search field (make it clearable and reactive to input).
+        */
+       DevlogListModule.initializeSearchField = function() {
+               $('#tx_devlog_search')
+                       .on('input', function() {
+                               DevlogListModule.table.search($(this).val()).draw();
+                       })
+                       .clearable({
+                               onClear: function() {
+                                       if (DevlogListModule.table !== null) {
+                                               DevlogListModule.table.search('').draw();
+                                       }
+                               }
+                       })
+                       .parents('form').on('submit', function() {
+                               return false;
+                       });
+       };
+
+       /**
+        * Initializes the extra data toggle buttons.
+        *
+        * @param tableView
+        */
+       DevlogListModule.initializeExtraDataToggle = function(tableView) {
+               // Single toggle button
+               tableView.on('click', 'button.extra-data-toggle', function() {
+                       DevlogListModule.toggleExtraData($(this));
+               });
+               // Global toggle button
+               tableView.on('click', '#tx_devlog_expand_all', function() {
+                       var toggleIcon = $('#tx_devlog_expand_all_icon');
+                       // Switch expand/collapse icon for global toggle
+                       if (toggleIcon.find('.t3js-icon').hasClass('icon-actions-view-list-expand')) {
+                               toggleIcon.html(DevlogListModule.collapseIcon);
+                       } else {
+                               toggleIcon.html(DevlogListModule.expandIcon);
+                       }
+                       // Loop on each individual toggle
+                       $('button.extra-data-toggle').each(function() {
+                               DevlogListModule.toggleExtraData($(this));
+                       });
+               });
+       };
+
+       /**
+        * Toggles extra data visibility for a given button.
+        *
+        * @param button
+        */
+       DevlogListModule.toggleExtraData = function(button) {
+               var extraDataWrapper = button.next('.extra-data-wrapper');
+               // Change visibility of extra data wrapper
+               extraDataWrapper.toggle();
+               // Switch expand/collapse icon
+               if (extraDataWrapper.is(':visible')) {
+                       button.html(DevlogListModule.collapseIcon);
+               } else {
+                       button.html(DevlogListModule.expandIcon);
+               }
+               button.blur();
+       };
+
+       /**
+        * Initializes the controls for manual reloading or automatic reloading
+        * of the table to read new records created since last update.
+        */
+       DevlogListModule.initializeReloadControls = function () {
+               // Handle reload action
+               $('#tx_devlog_reload').on('click', function () {
+                       DevlogListModule.loadNewRecords();
+               });
+               // Handle automatic reloading
+               $('#tx_devlog_autoreload').on('click', function () {
+                       // If no interval exists yet, activate reload automation
+                       if (DevlogListModule.intervalID === 0) {
+                               DevlogListModule.intervalID = window.setInterval(
+                                       DevlogListModule.loadNewRecords,
+                                       TYPO3.settings.DevLog.refreshFrequency * 1000
+                               );
+
+                       // If an interval already exists, clear it
+                       } else {
+                               window.clearInterval(DevlogListModule.intervalID);
+                               DevlogListModule.intervalID = 0;
+                       }
+               });
+       };
+
+       /**
+        * Loads records created since last update and refreshes table view.
+        */
+       DevlogListModule.loadNewRecords = function () {
+               // Activate loading mask
+               DevlogListModule.toggleLoadingMask();
+               // Fetch new records
+               $.ajax({
+                       url: TYPO3.settings.ajaxUrls['tx_devlog_reload'],
+                       data: {
+                               timestamp: DevlogListModule.lastUpdateTime
+                       },
+                       success: function (data, status, xhr) {
+                               // Update last update time
+                               DevlogListModule.lastUpdateTime = moment().unix();
+                               // Add records to DataTable
+                               DevlogListModule.table.rows.add(data).draw();
+                       },
+                       complete: function (xhr, status) {
+                               // Restore table view
+                               DevlogListModule.toggleLoadingMask();
+                       }
+               });
+       };
+
+       /**
+        * Toggles visibility of loading mask and table view
+        */
+       DevlogListModule.toggleLoadingMask = function() {
+               if (this.listWrapper.hasClass('hidden')) {
+                       this.listWrapper.removeClass('hidden');
+                       this.loadingMask.addClass('hidden');
+               } else {
+                       this.listWrapper.addClass('hidden');
+                       this.loadingMask.removeClass('hidden');
+               }
+
+       };
+
+       /**
+        * Initializes this module
+        */
+       $(function() {
+               var tableView = $('#tx_devlog_list');
+               DevlogListModule.init();
+               // @todo: how to ensure that all icons have been loaded? Deliver a new promise?
+               DevlogListModule.loadIcons();
+               DevlogListModule.buildDynamicTable(tableView);
+       });
+
+       return DevlogListModule;
+});
+
diff --git a/Resources/Public/JavaScript/datatables.mark.min.js b/Resources/Public/JavaScript/datatables.mark.min.js
new file mode 100644 (file)
index 0000000..cf68e6f
--- /dev/null
@@ -0,0 +1,7 @@
+/*!***************************************************
+ * datatables.mark.js v2.0.0
+ * https://github.com/julmot/datatables.mark.js
+ * Copyright (c) 2016, Julian Motz
+ * Released under the MIT license https://git.io/voRZ7
+ *****************************************************/
+"use strict";function _classCallCheck(a,b){if(!(a instanceof b))throw new TypeError("Cannot call a class as a function")}var _createClass=function(){function a(a,b){for(var c=0;c<b.length;c++){var d=b[c];d.enumerable=d.enumerable||!1,d.configurable=!0,"value"in d&&(d.writable=!0),Object.defineProperty(a,d.key,d)}}return function(b,c,d){return c&&a(b.prototype,c),d&&a(b,d),b}}(),_typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol?"symbol":typeof a};!function(a,b,c){"function"==typeof define&&define.amd?define(["jquery","datatables.net","markjs"],function(d){return a(b,c,d)}):"object"===("undefined"==typeof exports?"undefined":_typeof(exports))?(require("datatables.net"),require("markjs"),a(b,c,require("jquery"))):a(b,c,jQuery)}(function(a,b,c){var d=function(){function a(b,d){if(_classCallCheck(this,a),"function"!=typeof c.fn.mark||"function"!=typeof c.fn.unmark)throw new Error("jquery.mark.js is necessary for datatables.mark.js");this.instance=b,this.options="object"===("undefined"==typeof d?"undefined":_typeof(d))?d:{},this.intervalThreshold=49,this.intervalMs=300,this.initMarkListener()}return _createClass(a,[{key:"initMarkListener",value:function(){var a=this,b="draw.dt.dth column-visibility.dt.dth column-reorder.dt.dth",c=null;this.instance.on(b,function(){var b=a.instance.rows({filter:"applied",page:"current"}).nodes().length;b>a.intervalThreshold?(clearTimeout(c),c=setTimeout(function(){a.mark()},a.intervalMs)):a.mark()}),this.instance.on("destroy",function(){a.instance.off(b)}),this.mark()}},{key:"mark",value:function(){var a=this,b=this.instance.search();c(this.instance.table().body()).unmark(this.options),this.instance.columns({search:"applied",page:"current"}).nodes().each(function(d,e){var f=a.instance.column(e).search(),g=f||b;g&&d.forEach(function(b){c(b).mark(g,a.options)})})}}]),a}();c(b).on("init.dt.dth",function(a,b){if("dt"===a.namespace){var e=c.fn.dataTable.Api(b),f=null;e.init().mark?f=e.init().mark:c.fn.dataTable.defaults.mark&&(f=c.fn.dataTable.defaults.mark),null!==f&&new d(e,f)}})},window,document);
\ No newline at end of file
diff --git a/Resources/Public/JavaScript/jquery.mark.min.js b/Resources/Public/JavaScript/jquery.mark.min.js
new file mode 100644 (file)
index 0000000..9ca6775
--- /dev/null
@@ -0,0 +1,7 @@
+/*!***************************************************\r
+ * mark.js v8.4.0\r
+ * https://github.com/julmot/mark.js\r
+ * Copyright (c) 2014–2016, Julian Motz\r
+ * Released under the MIT license https://git.io/vwTVl\r
+ *****************************************************/\r
+"use strict";function _classCallCheck(a,b){if(!(a instanceof b))throw new TypeError("Cannot call a class as a function")}var _extends=Object.assign||function(a){for(var b=1;b<arguments.length;b++){var c=arguments[b];for(var d in c)Object.prototype.hasOwnProperty.call(c,d)&&(a[d]=c[d])}return a},_createClass=function(){function a(a,b){for(var c=0;c<b.length;c++){var d=b[c];d.enumerable=d.enumerable||!1,d.configurable=!0,"value"in d&&(d.writable=!0),Object.defineProperty(a,d.key,d)}}return function(b,c,d){return c&&a(b.prototype,c),d&&a(b,d),b}}(),_typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol?"symbol":typeof a};!function(a,b,c){"function"==typeof define&&define.amd?define(["jquery"],function(d){return a(b,c,d)}):"object"===("undefined"==typeof module?"undefined":_typeof(module))&&module.exports?module.exports=a(b,c,require("jquery")):a(b,c,jQuery)}(function(a,b,c){var d=function(){function c(a){_classCallCheck(this,c),this.ctx=a}return _createClass(c,[{key:"log",value:function a(b){var c=arguments.length<=1||void 0===arguments[1]?"debug":arguments[1],a=this.opt.log;this.opt.debug&&"object"===("undefined"==typeof a?"undefined":_typeof(a))&&"function"==typeof a[c]&&a[c]("mark.js: "+b)}},{key:"escapeStr",value:function(a){return a.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}},{key:"createRegExp",value:function(a){return a=this.escapeStr(a),Object.keys(this.opt.synonyms).length&&(a=this.createSynonymsRegExp(a)),this.opt.ignoreJoiners&&(a=this.setupIgnoreJoinersRegExp(a)),this.opt.diacritics&&(a=this.createDiacriticsRegExp(a)),a=this.createMergedBlanksRegExp(a),this.opt.ignoreJoiners&&(a=this.createIgnoreJoinersRegExp(a)),a=this.createAccuracyRegExp(a)}},{key:"createSynonymsRegExp",value:function(a){var b=this.opt.synonyms,c=this.opt.caseSensitive?"":"i";for(var d in b)if(b.hasOwnProperty(d)){var e=b[d],f=this.escapeStr(d),g=this.escapeStr(e);a=a.replace(new RegExp("("+f+"|"+g+")","gm"+c),"("+f+"|"+g+")")}return a}},{key:"setupIgnoreJoinersRegExp",value:function(a){return a.replace(/[^(|)]/g,function(a,b,c){var d=c.charAt(b+1);return/[(|)]/.test(d)||""===d?a:a+"\0"})}},{key:"createIgnoreJoinersRegExp",value:function(a){return a.split("\0").join("[\\u00ad|\\u200b|\\u200c|\\u200d]?")}},{key:"createDiacriticsRegExp",value:function(a){var b=this.opt.caseSensitive?"":"i",c=this.opt.caseSensitive?["aàáâãäåāąă","AÀÁÂÃÄÅĀĄĂ","cçćč","CÇĆČ","dđď","DĐĎ","eèéêëěēę","EÈÉÊËĚĒĘ","iìíîïī","IÌÍÎÏĪ","lł","LŁ","nñňń","NÑŇŃ","oòóôõöøō","OÒÓÔÕÖØŌ","rř","RŘ","sšśș","SŠŚȘ","tťț","TŤȚ","uùúûüůū","UÙÚÛÜŮŪ","yÿý","YŸÝ","zžżź","ZŽŻŹ"]:["aÀÁÂÃÄÅàáâãäåĀāąĄăĂ","cÇçćĆčČ","dđĐďĎ","eÈÉÊËèéêëěĚĒēęĘ","iÌÍÎÏìíîïĪī","lłŁ","nÑñňŇńŃ","oÒÓÔÕÖØòóôõöøŌō","rřŘ","sŠšśŚșȘ","tťŤțȚ","uÙÚÛÜùúûüůŮŪū","yŸÿýÝ","zŽžżŻźŹ"],d=[];return a.split("").forEach(function(e){c.every(function(c){if(c.indexOf(e)!==-1){if(d.indexOf(c)>-1)return!1;a=a.replace(new RegExp("["+c+"]","gm"+b),"["+c+"]"),d.push(c)}return!0})}),a}},{key:"createMergedBlanksRegExp",value:function(a){return a.replace(/[\s]+/gim,"[\\s]*")}},{key:"createAccuracyRegExp",value:function(a){var b=this,c=this.opt.accuracy,d="string"==typeof c?c:c.value,e="string"==typeof c?[]:c.limiters,f="";switch(e.forEach(function(a){f+="|"+b.escapeStr(a)}),d){case"partially":default:return"()("+a+")";case"complementary":return"()([^\\s"+f+"]*"+a+"[^\\s"+f+"]*)";case"exactly":return"(^|\\s"+f+")("+a+")(?=$|\\s"+f+")"}}},{key:"getSeparatedKeywords",value:function(a){var b=this,c=[];return a.forEach(function(a){b.opt.separateWordSearch?a.split(" ").forEach(function(a){a.trim()&&c.indexOf(a)===-1&&c.push(a)}):a.trim()&&c.indexOf(a)===-1&&c.push(a)}),{keywords:c.sort(function(a,b){return b.length-a.length}),length:c.length}}},{key:"getTextNodes",value:function(a){var b=this,c="",d=[];this.iterator.forEachNode(NodeFilter.SHOW_TEXT,function(a){d.push({start:c.length,end:(c+=a.textContent).length,node:a})},function(a){return b.matchesExclude(a.parentNode,!0)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT},function(){a({value:c,nodes:d})})}},{key:"matchesExclude",value:function(a,b){var c=this.opt.exclude.concat(["script","style","title","head","html"]);return b&&(c=c.concat(["*[data-markjs='true']"])),e.matches(a,c)}},{key:"wrapRangeInTextNode",value:function(a,c,d){var e=this.opt.element?this.opt.element:"mark",f=a.splitText(c),g=f.splitText(d-c),h=b.createElement(e);return h.setAttribute("data-markjs","true"),this.opt.className&&h.setAttribute("class",this.opt.className),h.textContent=f.textContent,f.parentNode.replaceChild(h,f),g}},{key:"wrapRangeInMappedTextNode",value:function(a,b,c,d,e){var f=this;a.nodes.every(function(g,h){var i=a.nodes[h+1];if("undefined"==typeof i||i.start>b){var j=function(){var i=b-g.start,j=(c>g.end?g.end:c)-g.start;if(d(g.node)){g.node=f.wrapRangeInTextNode(g.node,i,j);var k=a.value.substr(0,g.start),l=a.value.substr(j+g.start);if(a.value=k+l,a.nodes.forEach(function(b,c){c>=h&&(a.nodes[c].start>0&&c!==h&&(a.nodes[c].start-=j),a.nodes[c].end-=j)}),c-=j,e(g.node.previousSibling,g.start),!(c>g.end))return{v:!1};b=g.end}}();if("object"===("undefined"==typeof j?"undefined":_typeof(j)))return j.v}return!0})}},{key:"wrapMatches",value:function(a,b,c,d,e){var f=this,g=0===b?0:b+1;this.getTextNodes(function(b){b.nodes.forEach(function(b){b=b.node;for(var e=void 0;null!==(e=a.exec(b.textContent))&&""!==e[g];)if(c(e[g],b)){var h=e.index;if(0!==g)for(var i=1;i<g;i++)h+=e[i].length;b=f.wrapRangeInTextNode(b,h,h+e[g].length),d(b.previousSibling),a.lastIndex=0}}),e()})}},{key:"wrapMatchesAcrossElements",value:function(a,b,c,d,e){var f=this,g=0===b?0:b+1;this.getTextNodes(function(b){for(var h=void 0;null!==(h=a.exec(b.value))&&""!==h[g];){var i=h.index;if(0!==g)for(var j=1;j<g;j++)i+=h[j].length;var k=i+h[g].length;f.wrapRangeInMappedTextNode(b,i,k,function(a){return c(h[g],a)},function(b,c){a.lastIndex=c,d(b)})}e()})}},{key:"unwrapMatches",value:function(a){for(var c=a.parentNode,d=b.createDocumentFragment();a.firstChild;)d.appendChild(a.removeChild(a.firstChild));c.replaceChild(d,a),c.normalize()}},{key:"markRegExp",value:function(a,b){var c=this;this.opt=b,this.log('Searching with expression "'+a+'"');var d=0,e="wrapMatches",f=function(a){d++,c.opt.each(a)};this.opt.acrossElements&&(e="wrapMatchesAcrossElements"),this[e](a,this.opt.ignoreGroups,function(a,b){return c.opt.filter(b,a,d)},f,function(){0===d&&c.opt.noMatch(a),c.opt.done(d)})}},{key:"mark",value:function(a,b){var c=this;this.opt=b;var d=0,e="wrapMatches",f=this.getSeparatedKeywords("string"==typeof a?[a]:a),g=f.keywords,h=f.length,i=this.opt.caseSensitive?"":"i",j=function a(b){var f=new RegExp(c.createRegExp(b),"gm"+i),j=0;c.log('Searching with expression "'+f+'"'),c[e](f,1,function(a,e){return c.opt.filter(e,b,d,j)},function(a){j++,d++,c.opt.each(a)},function(){0===j&&c.opt.noMatch(b),g[h-1]===b?c.opt.done(d):a(g[g.indexOf(b)+1])})};this.opt.acrossElements&&(e="wrapMatchesAcrossElements"),0===h?this.opt.done(d):j(g[0])}},{key:"unmark",value:function(a){var b=this;this.opt=a;var c=this.opt.element?this.opt.element:"*";c+="[data-markjs]",this.opt.className&&(c+="."+this.opt.className),this.log('Removal selector "'+c+'"'),this.iterator.forEachNode(NodeFilter.SHOW_ELEMENT,function(a){b.unwrapMatches(a)},function(a){var d=e.matches(a,c),f=b.matchesExclude(a,!1);return!d||f?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT},this.opt.done)}},{key:"opt",set:function(b){this._opt=_extends({},{element:"",className:"",exclude:[],iframes:!1,separateWordSearch:!0,diacritics:!0,synonyms:{},accuracy:"partially",acrossElements:!1,caseSensitive:!1,ignoreJoiners:!1,ignoreGroups:0,each:function(){},noMatch:function(){},filter:function(){return!0},done:function(){},debug:!1,log:a.console},b)},get:function(){return this._opt}},{key:"iterator",get:function(){return this._iterator||(this._iterator=new e(this.ctx,this.opt.iframes,this.opt.exclude)),this._iterator}}]),c}(),e=function(){function a(b){var c=arguments.length<=1||void 0===arguments[1]||arguments[1],d=arguments.length<=2||void 0===arguments[2]?[]:arguments[2];_classCallCheck(this,a),this.ctx=b,this.iframes=c,this.exclude=d}return _createClass(a,[{key:"getContexts",value:function(){var a=void 0,b=[];return a="undefined"!=typeof this.ctx&&this.ctx?NodeList.prototype.isPrototypeOf(this.ctx)?Array.prototype.slice.call(this.ctx):Array.isArray(this.ctx)?this.ctx:[this.ctx]:[],a.forEach(function(a){var c=b.filter(function(b){return b.contains(a)}).length>0;b.indexOf(a)!==-1||c||b.push(a)}),b}},{key:"getIframeContents",value:function(a,b){var c=arguments.length<=2||void 0===arguments[2]?function(){}:arguments[2],d=void 0;try{var e=a.contentWindow;if(d=e.document,!e||!d)throw new Error("iframe inaccessible")}catch(a){c()}d&&b(d)}},{key:"onIframeReady",value:function(a,b,c){var d=this;try{!function(){var e=a.contentWindow,f="about:blank",g="complete",h=function(){var b=a.getAttribute("src").trim(),c=e.location.href;return c===f&&b!==f&&b},i=function(){var e=function e(){try{h()||(a.removeEventListener("load",e),d.getIframeContents(a,b,c))}catch(a){c()}};a.addEventListener("load",e)};e.document.readyState===g?h()?i():d.getIframeContents(a,b,c):i()}()}catch(a){c()}}},{key:"waitForIframes",value:function(a,b){var c=this,d=0;this.forEachIframe(a,function(){return!0},function(a){d++,c.waitForIframes(a.querySelector("html"),function(){--d||b()})},function(a){a||b()})}},{key:"forEachIframe",value:function(b,c,d){var e=this,f=arguments.length<=3||void 0===arguments[3]?function(){}:arguments[3],g=b.querySelectorAll("iframe"),h=g.length,i=0;g=Array.prototype.slice.call(g);var j=function(){--h<=0&&f(i)};h||j(),g.forEach(function(b){a.matches(b,e.exclude)?j():e.onIframeReady(b,function(a){c(b)&&(i++,d(a)),j()},j)})}},{key:"createIterator",value:function(a,c,d){return b.createNodeIterator(a,c,d,!1)}},{key:"createInstanceOnIframe",value:function(b){return new a(b.querySelector("html"),this.iframes)}},{key:"compareNodeIframe",value:function(a,b,c){var d=a.compareDocumentPosition(c),e=Node.DOCUMENT_POSITION_PRECEDING;if(d&e){if(null===b)return!0;var f=b.compareDocumentPosition(c),g=Node.DOCUMENT_POSITION_FOLLOWING;if(f&g)return!0}return!1}},{key:"getIteratorNode",value:function(a){var b=a.previousNode(),c=void 0;return c=null===b?a.nextNode():a.nextNode()&&a.nextNode(),{prevNode:b,node:c}}},{key:"checkIframeFilter",value:function(a,b,c,d){var e=!1,f=!1;return d.forEach(function(a,b){a.val===c&&(e=b,f=a.handled)}),this.compareNodeIframe(a,b,c)?(e!==!1||f?e===!1||f||(d[e].handled=!0):d.push({val:c,handled:!0}),!0):(e===!1&&d.push({val:c,handled:!1}),!1)}},{key:"handleOpenIframes",value:function(a,b,c,d){var e=this;a.forEach(function(a){a.handled||e.getIframeContents(a.val,function(a){e.createInstanceOnIframe(a).forEachNode(b,c,d)})})}},{key:"iterateThroughNodes",value:function(a,b,c,d,e){for(var f=this,g=this.createIterator(b,a,d),h=[],i=void 0,j=void 0,k=function(){var a=f.getIteratorNode(g);return j=a.prevNode,i=a.node};k();)this.iframes&&this.forEachIframe(b,function(a){return f.checkIframeFilter(i,j,a,h)},function(b){f.createInstanceOnIframe(b).forEachNode(a,c,d)}),c(i);this.iframes&&this.handleOpenIframes(h,a,c,d),e()}},{key:"forEachNode",value:function(a,b,c){var d=this,e=arguments.length<=3||void 0===arguments[3]?function(){}:arguments[3],f=this.getContexts(),g=f.length;g||e(),f.forEach(function(f){var h=function(){d.iterateThroughNodes(a,f,b,c,function(){--g<=0&&e()})};d.iframes?d.waitForIframes(f,h):h()})}}],[{key:"matches",value:function(a,b){var c="string"==typeof b?[b]:b,d=a.matches||a.matchesSelector||a.msMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.webkitMatchesSelector;if(d){var e=!1;return c.every(function(b){return!d.call(a,b)||(e=!0,!1)}),e}return!1}}]),a}();return c.fn.mark=function(a,b){return new d(this.get()).mark(a,b),this},c.fn.markRegExp=function(a,b){return new d(this.get()).markRegExp(a,b),this},c.fn.unmark=function(a){return new d(this.get()).unmark(a),this},c},window,document);
\ No newline at end of file
diff --git a/Resources/Public/StyleSheet/Devlog.css b/Resources/Public/StyleSheet/Devlog.css
new file mode 100644 (file)
index 0000000..5e2749d
--- /dev/null
@@ -0,0 +1,41 @@
+/* Styles for the BE module */
+
+.sorting,
+.sorting_asc,
+.sorting_desc {
+       cursor: pointer;
+}
+.sorting_asc,
+.sorting_desc {
+       background-color: #c9c9c9;
+}
+
+th#tx_devlog_expand_all {
+       cursor: pointer;
+}
+
+a.paginate_button {
+       border-top: 1px solid #cccccc;
+       border-bottom: 1px solid #cccccc;
+       border-left: 1px solid #cccccc;
+       padding: 8px;
+       cursor: pointer;
+}
+a.previous {
+       border-bottom-left-radius: 2px;
+       border-top-left-radius: 2px;
+}
+a.next {
+       border-right: 1px solid #cccccc;
+       border-bottom-right-radius: 2px;
+       border-top-right-radius: 2px;
+}
+
+.form-row {
+       margin-bottom: 15px;
+}
+
+mark {
+       background-color: #e8a33d;
+       color: #ffffff;
+}
\ No newline at end of file
diff --git a/Tests/Functional/Domain/Repository/EntryRepositoryTest.php b/Tests/Functional/Domain/Repository/EntryRepositoryTest.php
new file mode 100644 (file)
index 0000000..b4a0292
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+namespace Devlog\Devlog\Tests\Functional\Domain\Repository;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Devlog\Devlog\Domain\Repository\EntryRepository;
+use TYPO3\CMS\Extbase\Object\ObjectManager;
+
+/**
+ * Functional tests for Entry repository.
+ *
+ * @package Devlog\Devlog\Tests\Functional\Domain\Repository
+ */
+class EntryRepositoryTest extends \Tx_Phpunit_Database_TestCase
+{
+    /**
+     * @var EntryRepository
+     */
+    protected $subject = null;
+
+    protected function setUp()
+    {
+        if (!$this->createDatabase()) {
+            self::markTestSkipped('Test database could not be created.');
+        }
+        $this->importExtensions(['devlog']);
+
+        $objectManager = new ObjectManager();
+        $this->subject = $objectManager->get(EntryRepository::class);
+    }
+
+    /**
+     * @test
+     */
+    public function findAllReturnsNothingForEmptyDatabase()
+    {
+        $records = $this->subject->findAll();
+        self::assertCount(
+                0,
+                $records
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function findAllReturnsAllRecords()
+    {
+        $this->importDataSet(__DIR__ . '/Fixtures/DevlogEntries.xml');
+
+        $records = $this->subject->findAll();
+        self::assertCount(
+                2,
+                $records
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function findAfterDateReturnsOnlyNewRecords()
+    {
+        $this->importDataSet(__DIR__ . '/Fixtures/DevlogEntries.xml');
+
+        // Find entries after July 15, 2016
+        $records = $this->subject->findAfterDate(1468579836);
+        self::assertCount(
+                1,
+                $records
+        );
+    }
+}
\ No newline at end of file
diff --git a/Tests/Functional/Domain/Repository/Fixtures/DevlogEntries.xml b/Tests/Functional/Domain/Repository/Fixtures/DevlogEntries.xml
new file mode 100644 (file)
index 0000000..e26dbc9
--- /dev/null
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<dataset>
+       <!-- "old" entry -->
+       <tx_devlog_domain_model_entry>
+               <uid>1</uid>
+               <pid>0</pid>
+               <run_id>14677290780.15759200</run_id>
+               <sorting>1</sorting>
+               <severity>0</severity>
+               <extkey>devlog</extkey>
+               <message>This is a fixture entry</message>
+               <extra_data></extra_data>
+               <location>DevlongEntries.xml</location>
+               <line>1</line>
+               <ip>::1</ip>
+               <crdate>1467729077</crdate>
+               <cruser_id>1</cruser_id>
+       </tx_devlog_domain_model_entry>
+
+       <!-- "new" entry from July 31, 2016 -->
+       <tx_devlog_domain_model_entry>
+               <uid>2</uid>
+               <pid>0</pid>
+               <run_id>14699620710.15759200</run_id>
+               <sorting>1</sorting>
+               <severity>1</severity>
+               <extkey>devlog</extkey>
+               <message>This is a new fixture entry</message>
+               <extra_data></extra_data>
+               <location>DevlongEntries.xml</location>
+               <line>2</line>
+               <ip>::1</ip>
+               <crdate>1469962071</crdate>
+               <cruser_id>1</cruser_id>
+       </tx_devlog_domain_model_entry>
+</dataset>
\ No newline at end of file
index 73b424d..52cc50e 100644 (file)
@@ -6,28 +6,28 @@
 # customsubcategory=filtering=LLL:EXT:devlog/Resources/Private/Language/locallang_configuration.xlf:filtering
 # customsubcategory=display=LLL:EXT:devlog/Resources/Private/Language/locallang_configuration.xlf:display
 
-# cat=general/filtering/; type=options[LLL:EXT:devlog/Resources/Private/Language/locallang.xlf:status_ok=-1,LLL:EXT:devlog/Resources/Private/Language/locallang.xlf:status_information=0,LLL:EXT:devlog/Resources/Private/Language/locallang.xlf:status_notice=1,LLL:EXT:devlog/Resources/Private/Language/locallang.xlf:status_warning=2,LLL:EXT:devlog/Resources/Private/Language/locallang.xlf:status_error=3]; label=LLL:EXT:devlog/Resources/Private/Language/locallang_configuration.xlf:minimum_level
+# cat=general/filtering/a; type=options[LLL:EXT:devlog/Resources/Private/Language/locallang.xlf:status_ok=-1,LLL:EXT:devlog/Resources/Private/Language/locallang.xlf:status_information=0,LLL:EXT:devlog/Resources/Private/Language/locallang.xlf:status_notice=1,LLL:EXT:devlog/Resources/Private/Language/locallang.xlf:status_warning=2,LLL:EXT:devlog/Resources/Private/Language/locallang.xlf:status_error=3]; label=LLL:EXT:devlog/Resources/Private/Language/locallang_configuration.xlf:minimum_level
 minimumLogLevel = -1
 
-# cat=general/filtering/; type=string; label=LLL:EXT:devlog/Resources/Private/Language/locallang_configuration.xlf:excluded_keys
+# cat=general/filtering/b; type=string; label=LLL:EXT:devlog/Resources/Private/Language/locallang_configuration.xlf:excluded_keys
 excludeKeys =
 
-# cat=general/filtering/; type=string; label=LLL:EXT:devlog/Resources/Private/Language/locallang_configuration.xlf:ip_filter
+# cat=general/filtering/c; type=string; label=LLL:EXT:devlog/Resources/Private/Language/locallang_configuration.xlf:ip_filter
 ipFilter =
 
-# cat=general/display/; type=integer; label=Autorefresh frequency: Set the number of seconds between each refresh, when using the autorefresh feature
-refreshFrequency = 2
+# cat=general/display/a; type=integer; label=Autorefresh frequency: Set the number of seconds between each refresh, when using the autorefresh feature
+refreshFrequency = 4
 
-# cat=general/display/; type=integer; label=Number of entries per page: Set the number of log entries to display per page, when viewing all log entries
+# cat=general/display/b; type=integer; label=Number of entries per page: Set the number of log entries to display per page, when viewing all log entries
 entriesPerPage = 25
 
-# cat=dbwriter/limits/; type=integer; label=LLL:EXT:devlog/Resources/Private/Language/locallang_configuration.xlf:maximum_rows
+# cat=dbwriter/limits/c; type=integer; label=LLL:EXT:devlog/Resources/Private/Language/locallang_configuration.xlf:maximum_rows
 maximumRows = 1000
 
-# cat=dbwriter/limits/; type=boolean; label=LLL:EXT:devlog/Resources/Private/Language/locallang_configuration.xlf:optimize
+# cat=dbwriter/limits/a; type=boolean; label=LLL:EXT:devlog/Resources/Private/Language/locallang_configuration.xlf:optimize
 optimizeTable = 1
 
-# cat=dbwriter/limits/; type=integer; label=LLL:EXT:devlog/Resources/Private/Language/locallang_configuration.xlf:maximum_extra_data_size
+# cat=dbwriter/limits/b; type=integer; label=LLL:EXT:devlog/Resources/Private/Language/locallang_configuration.xlf:maximum_extra_data_size
 maximumExtraDataSize = 1000000
 
 # cat=filewriter//; type=string; label=LLL:EXT:devlog/Resources/Private/Language/locallang_configuration.xlf:log_file_path
index 7d02bb5..20b9af9 100644 (file)
@@ -4,7 +4,7 @@ if (!defined ('TYPO3_MODE')) {
 }
 
 // Register the logging method with the appropriate hook
-$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_div.php']['devLog'][$_EXTKEY] = \Devlog\Devlog\Utility\Logger::class . '->log';
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_div.php']['devLog']['tx_devlog'] = \Devlog\Devlog\Utility\Logger::class . '->log';
 
 // Register log writers
 $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['devlog']['writers']['db'] = \Devlog\Devlog\Writer\DatabaseWriter::class;
index 3a977b3..0ae895d 100644 (file)
@@ -5,8 +5,31 @@ if (!defined('TYPO3_MODE')) {
 
 \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::allowTableOnStandardPages('tx_devlog_domain_model_entry');
 
-// Add context sensitive help (csh) to the backend module and to the tx_devlog table
+// Add context sensitive help (csh) to the devlog table
 \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addLLrefForTCAdescr(
         'tx_devlog_domain_model_entry',
         'EXT:devlog/Resources/Private/Language/locallang_csh_txdevlog.xlf'
 );
+
+// Load the module only in the BE context
+if (TYPO3_MODE === 'BE') {
+    // Register the "Data Import" backend module
+    \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerModule(
+            'Devlog.Devlog',
+            // Make it a submodule of 'ExternalImport'
+            'system',
+            // Submodule key
+            'devlog',
+            // Position
+            'after:BelogLog',
+            array(
+                // An array holding the controller-action-combinations that are accessible
+                'ListModule' => 'index, get'
+            ),
+            array(
+                    'access' => 'admin',
+                    'icon' => 'EXT:' . $_EXTKEY . '/Resources/Public/Images/ModuleIcon.svg',
+                    'labels' => 'LLL:EXT:' . $_EXTKEY . '/Resources/Private/Language/Module.xlf'
+            )
+    );
+}