[FEATURE] Restore log clearing feature 43/50443/3
authorFrancois Suter <francois@typo3.org>
Wed, 12 Oct 2016 08:55:33 +0000 (10:55 +0200)
committerFrancois Suter <francois@typo3.org>
Sun, 30 Oct 2016 20:19:34 +0000 (21:19 +0100)
Recreate the log clearing feature in the new BE module.
Instead of a separate view, make it a menu with AJAX actions
for clearing log entries.

Change-Id: I97469331d292f94a5386797528299dbd9df43707
Resolves: #76972
Releases: 3.0
Reviewed-on: https://review.typo3.org/50443
Reviewed-by: Francois Suter <francois@typo3.org>
Tested-by: Francois Suter <francois@typo3.org>
15 files changed:
ChangeLog
Classes/Controller/ListModuleController.php
Classes/Domain/Repository/EntryRepository.php
Classes/Utility/Logger.php
Configuration/Backend/AjaxRoutes.php
Resources/Private/Language/Configuration.xlf [new file with mode: 0644]
Resources/Private/Language/JavaScript.xlf [new file with mode: 0644]
Resources/Private/Language/locallang.xlf
Resources/Private/Language/locallang_configuration.xlf [deleted file]
Resources/Private/Templates/ListModule/Index.html
Resources/Public/JavaScript/ListModule.js
Tests/Functional/Domain/Repository/EntryRepositoryTest.php
Tests/Functional/Domain/Repository/Fixtures/DevlogEntries.xml
ext_conf_template.txt
ext_tables.php

index 09e0db2..6078b66 100755 (executable)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,7 @@
+2016-10-30 Francois Suter <typo3@cobweb.ch>
+
+       * Restored and improved log clearing feature, resolves #76972
+
 2016-09-04 Francois Suter <typo3@cobweb.ch>
 
        * Restored column filters in new backend module, resolves #76971
index 64769ec..2ed5f00 100644 (file)
@@ -20,12 +20,16 @@ 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\Template\Components\Menu\Menu;
+use TYPO3\CMS\Backend\Template\Components\Menu\MenuItem;
 use TYPO3\CMS\Backend\View\BackendTemplateView;
 use TYPO3\CMS\Core\Imaging\Icon;
+use TYPO3\CMS\Core\Messaging\AbstractMessage;
 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\Mvc\Web\Routing\UriBuilder;
 use TYPO3\CMS\Extbase\Object\ObjectManager;
 use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
 
@@ -108,6 +112,11 @@ class ListModuleController extends ActionController
      */
     protected function initializeView(ViewInterface $view)
     {
+        // If the action is "delete", exit early
+        if ($this->actionMethodName === 'deleteAction') {
+            return;
+        }
+
         if ($view instanceof BackendTemplateView) {
             parent::initializeView($view);
         }
@@ -120,16 +129,8 @@ class ListModuleController extends ActionController
                 'DevLog',
                 $this->extensionConfiguration->toArray()
         );
-        $pageRenderer->addInlineLanguageLabelArray(
-                array(
-                        'severity-1' => 'LLL:EXT:devlog/Resources/Private/Language/locallang.xlf:severity_ok',
-                        'severity0' => 'LLL:EXT:devlog/Resources/Private/Language/locallang.xlf:severity_info',
-                        'severity1' => 'LLL:EXT:devlog/Resources/Private/Language/locallang.xlf:severity_notification',
-                        'severity2' => 'LLL:EXT:devlog/Resources/Private/Language/locallang.xlf:severity_warning',
-                        'severity3' => 'LLL:EXT:devlog/Resources/Private/Language/locallang.xlf:severity_error',
-                ),
-                true
-        );
+        $pageRenderer->addInlineLanguageLabelFile('EXT:devlog/Resources/Private/Language/JavaScript.xlf');
+
         // Add open in new window button
         $newWindowIcon = $this->view->getModuleTemplate()->getIconFactory()->getIcon('actions-window-open', Icon::SIZE_SMALL);
         $newWindowButton = GeneralUtility::makeInstance(ExtendedLinkButton::class);
@@ -143,6 +144,35 @@ class ListModuleController extends ActionController
                 $newWindowButton,
                 ButtonBar::BUTTON_POSITION_RIGHT
         );
+
+        // Add clear log menu
+        /** @var Menu $menu */
+        $menu = GeneralUtility::makeInstance(Menu::class);
+        $menu->setIdentifier('_devlogClearMenu');
+
+        /** @var UriBuilder $uriBuilder */
+        $uriBuilder = $this->objectManager->get(UriBuilder::class);
+        $uriBuilder->setRequest($this->request);
+
+        // This menu originally has a single item
+        // Additional items are created on the fly (per JavaScript) depending on the log entries in the table
+        /** @var MenuItem $clearLogMenuItem */
+        $clearLogMenuItem = GeneralUtility::makeInstance(MenuItem::class);
+        $clearLogMenuItem->setTitle(
+                LocalizationUtility::translate(
+                        'clearlog',
+                        'devlog'
+                )
+        );
+        $uri = $uriBuilder->reset()->uriFor(
+                'index',
+                array(),
+                'ListModule'
+        );
+        $clearLogMenuItem->setActive(true)->setHref($uri);
+
+        $menu->addMenuItem($clearLogMenuItem);
+        $this->view->getModuleTemplate()->getDocHeaderComponent()->getMenuRegistry()->addMenu($menu);
     }
 
     /**
@@ -156,6 +186,77 @@ class ListModuleController extends ActionController
     }
 
     /**
+     * Deletes log entries and redirects to list view.
+     *
+     * @param string $clear Type of deletion to perform ("all", "key" or "period")
+     * @param string $value Subtype of deletion to perform (for "key" and "period" types)
+     * @return void
+     */
+    public function deleteAction($clear, $value = '')
+    {
+        $deletedEntries = 0;
+        switch ($clear) {
+            case 'all':
+                $deletedEntries = $this->entryRepository->deleteAll();
+                break;
+            case 'key':
+                $deletedEntries = $this->entryRepository->deleteByKey($value);
+                break;
+            case 'period':
+                $constant = EntryRepository::class . '::' . 'PERIOD_' . strtoupper($value);
+                // Valid period, clear accordingly
+                if (defined($constant)) {
+                    $deletedEntries = $this->entryRepository->deleteByPeriod(constant($constant));
+                // Invalid period, prepare error message
+                } else {
+                    $this->addFlashMessage(
+                            LocalizationUtility::translate(
+                                    'clearlog_invalid_period_error',
+                                    'devlog'
+                            ),
+                            '',
+                            AbstractMessage::ERROR
+                    );
+                    // Return to default view
+                    $this->redirect('index');
+                }
+                break;
+            default:
+                // Invalid action, prepare error message
+                $this->addFlashMessage(
+                        LocalizationUtility::translate(
+                                'clearlog_action_error',
+                                'devlog'
+                        ),
+                        '',
+                        AbstractMessage::ERROR
+                );
+                // Return to default view
+                $this->redirect('index');
+        }
+        // Report on number of entries deleted
+        if ($deletedEntries > 0) {
+            $this->addFlashMessage(
+                    LocalizationUtility::translate(
+                            'cleared_log',
+                            'devlog',
+                            array($deletedEntries)
+                    )
+            );
+        } else {
+            $this->addFlashMessage(
+                    LocalizationUtility::translate(
+                            'clearlog_nothing_cleared',
+                            'devlog',
+                            array($deletedEntries)
+                    )
+            );
+        }
+        // Return to default view
+        $this->redirect('index');
+    }
+
+    /**
      * Returns the list of all log entries, in JSON format.
      *
      * @param ServerRequestInterface $request
@@ -193,4 +294,28 @@ class ListModuleController extends ActionController
         $response->getBody()->write(json_encode($entries));
         return $response;
     }
+
+    /**
+     * Returns a count of log entries, based on various grouping criteria, in JSON format.
+     *
+     * @param ServerRequestInterface $request
+     * @param ResponseInterface $response
+     * @return ResponseInterface
+     */
+    public function getCountAction(ServerRequestInterface $request, ResponseInterface $response)
+    {
+        $this->initializeForAjaxAction();
+
+        $countByKey = $this->entryRepository->countByKey();
+        $countTotal = array_sum($countByKey);
+        $counts = array(
+                'all' => $countTotal,
+                'keys' => $countByKey,
+                'periods' => $this->entryRepository->countByPeriod()
+        );
+
+        // Send the response
+        $response->getBody()->write(json_encode($counts));
+        return $response;
+    }
 }
\ No newline at end of file
index 28276f2..04d4122 100644 (file)
@@ -29,6 +29,17 @@ use TYPO3\CMS\Core\SingletonInterface;
 class EntryRepository implements SingletonInterface
 {
     /**
+     * Periods for log clearing intervals
+     */
+    const PERIOD_1YEAR = 31536000;
+    const PERIOD_6MONTHS = 15768000;
+    const PERIOD_3MONTHS = 7884000;
+    const PERIOD_1MONTH = 2592000;
+    const PERIOD_1WEEK = 604800;
+    const PERIOD_1DAY = 86400;
+    const PERIOD_1HOUR = 3600;
+
+    /**
      * @var string Name of the database table used for logging
      */
     protected $databaseTable = 'tx_devlog_domain_model_entry';
@@ -92,6 +103,130 @@ class EntryRepository implements SingletonInterface
     }
 
     /**
+     * Finds all entries for a given key.
+     *
+     * @param string $key The key to look for
+     * @return array
+     */
+    public function findByKey($key)
+    {
+        try {
+            $entries = $this->getDatabaseConnection()->exec_SELECTgetRows(
+                    '*',
+                    $this->databaseTable,
+                    'extkey = ' . $this->getDatabaseConnection()->fullQuoteStr(
+                            $key,
+                            $this->databaseTable
+                    ),
+                    '',
+                    'crdate DESC, sorting ASC'
+            );
+        } catch (\Exception $e) {
+            $entries = array();
+        }
+        $entries = $this->expandEntryData($entries);
+        return $entries;
+    }
+
+    /**
+     * Returns the number of entries per key.
+     *
+     * @return array
+     */
+    public function countByKey()
+    {
+        $count = array();
+        try {
+            $rows = $this->getDatabaseConnection()->exec_SELECTgetRows(
+                    'extkey, COUNT(uid) AS total',
+                    $this->databaseTable,
+                    '',
+                    'extkey',
+                    'extkey'
+            );
+            if (is_array($rows)) {
+                /** @var array $rows */
+                foreach ($rows as $row) {
+                    $count[$row['extkey']] = (int)$row['total'];
+                }
+            }
+        } catch (\Exception $e) {
+            // Let an empty array return
+        }
+        return $count;
+    }
+
+    /**
+     * Returns the number of entries for predefined periods of time.
+     *
+     * @return array
+     */
+    public function countByPeriod()
+    {
+        $count = array(
+                '1hour' => 0,
+                '1day' => 0,
+                '1week' => 0,
+                '1month' => 0,
+                '3months' => 0,
+                '6months' => 0,
+                '1year' => 0
+        );
+        try {
+            $now = time();
+            $rows = $this->getDatabaseConnection()->exec_SELECTgetRows(
+                    '(' . $now . ' - crdate) AS age',
+                    $this->databaseTable,
+                    ''
+            );
+            if (is_array($rows)) {
+                /** @var array $rows */
+                foreach ($rows as $row) {
+                    if ($row['age'] >= self::PERIOD_1YEAR) {
+                        $count['1hour']++;
+                        $count['1day']++;
+                        $count['1week']++;
+                        $count['1month']++;
+                        $count['3months']++;
+                        $count['6months']++;
+                        $count['1year']++;
+                    } elseif ($row['age'] >= self::PERIOD_6MONTHS) {
+                        $count['1hour']++;
+                        $count['1day']++;
+                        $count['1week']++;
+                        $count['1month']++;
+                        $count['3months']++;
+                        $count['6months']++;
+                    } elseif ($row['age'] >= self::PERIOD_3MONTHS) {
+                        $count['1hour']++;
+                        $count['1day']++;
+                        $count['1week']++;
+                        $count['1month']++;
+                        $count['3months']++;
+                    } elseif ($row['age'] >= self::PERIOD_1MONTH) {
+                        $count['1hour']++;
+                        $count['1day']++;
+                        $count['1week']++;
+                        $count['1month']++;
+                    } elseif ($row['age'] >= self::PERIOD_1WEEK) {
+                        $count['1hour']++;
+                        $count['1day']++;
+                        $count['1week']++;
+                    } elseif ($row['age'] >= self::PERIOD_1DAY) {
+                        $count['1hour']++;
+                        $count['1day']++;
+                    } elseif ($row['age'] >= self::PERIOD_1HOUR) {
+                        $count['1hour']++;
+                    }
+                }
+            }
+        } catch (\Exception $e) {
+            // Let an empty array return
+        }
+        return $count;
+    }
+
+    /**
      * Adds a log entry to the database table.
      *
      * @param \Devlog\Devlog\Domain\Model\Entry $entry
@@ -182,6 +317,55 @@ class EntryRepository implements SingletonInterface
     }
 
     /**
+     * Deletes all log entries in the database table.
+     *
+     * @return int
+     */
+    public function deleteAll()
+    {
+        // Since we use TRUNCATE, count the number of records first, to return as number of deleted records
+        $deleted = $this->getDatabaseConnection()->exec_SELECTcountRows(
+                'uid',
+                $this->databaseTable
+        );
+        $this->getDatabaseConnection()->exec_TRUNCATEquery($this->databaseTable);
+        return $deleted;
+    }
+
+    /**
+     * Deletes all log entries related to the given key.
+     *
+     * @param string $key The key to look for
+     * @return int
+     */
+    public function deleteByKey($key)
+    {
+        $this->getDatabaseConnection()->exec_DELETEquery(
+                $this->databaseTable,
+                'extkey = ' . $this->getDatabaseConnection()->fullQuoteStr(
+                        $key,
+                        $this->databaseTable
+                )
+        );
+        return $this->getDatabaseConnection()->sql_affected_rows();
+    }
+
+    /**
+     * Deletes all log entries older than the given period.
+     *
+     * @param int $period Age of log entries which should be deleted
+     * @return int
+     */
+    public function deleteByPeriod($period)
+    {
+        $this->getDatabaseConnection()->exec_DELETEquery(
+                $this->databaseTable,
+                'crdate <= ' . (time() - (int)$period)
+        );
+        return $this->getDatabaseConnection()->sql_affected_rows();
+    }
+
+    /**
      * Collects additional data or transforms data from entries for simpler handling during display.
      *
      * @param array $entries
index be520c1..f771ab8 100644 (file)
@@ -118,7 +118,7 @@ class Logger implements SingletonInterface
                 $this->counter
         );
         $this->counter++;
-        $entry->setCrdate($GLOBALS['EXEC_TIME']);
+        $entry->setCrdate(time());
         $entry->setMessage(
                 GeneralUtility::removeXSS($logData['msg'])
         );
index d43d15b..a4a2264 100644 (file)
@@ -8,5 +8,9 @@ return [
         'tx_devlog_reload' => [
                 'path' => '/devlog/list/reload',
                 'target' => \Devlog\Devlog\Controller\ListModuleController::class . '::getNewAction'
+        ],
+        'tx_devlog_count' => [
+                'path' => '/devlog/list/count',
+                'target' => \Devlog\Devlog\Controller\ListModuleController::class . '::getCountAction'
         ]
 ];
diff --git a/Resources/Private/Language/Configuration.xlf b/Resources/Private/Language/Configuration.xlf
new file mode 100644 (file)
index 0000000..a5841b4
--- /dev/null
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<xliff version="1.0">
+       <file source-language="en" datatype="plaintext" original="messages" date="2014-10-15T16:49:33Z" product-name="devlog">
+               <header/>
+               <body>
+                       <trans-unit id="general">
+                               <source>General</source>
+                       </trans-unit>
+                       <trans-unit id="db_writer">
+                               <source>Database Writer</source>
+                       </trans-unit>
+                       <trans-unit id="file_writer">
+                               <source>File Writer</source>
+                       </trans-unit>
+                       <trans-unit id="limits">
+                               <source>Limits</source>
+                       </trans-unit>
+                       <trans-unit id="filtering">
+                               <source>Filtering</source>
+                       </trans-unit>
+                       <trans-unit id="display">
+                               <source>Display</source>
+                       </trans-unit>
+                       <trans-unit id="minimum_level">
+                               <source>Minimum level for logging: Minimum message level required for actually writing to the logs.</source>
+                       </trans-unit>
+                       <trans-unit id="excluded_keys">
+                               <source>Excluded keys: Comma-separated list of (extension) keys whose entries should not be written to the logs.</source>
+                       </trans-unit>
+                       <trans-unit id="ip_filter">
+                               <source>Restricted IP addresses: Only requests coming from the given IP addresses will be logged. An empty value will block everything, a "*" will allow everything. Use "devIPmask" (not case-sensitive) to reuse devIPmask value.</source>
+                       </trans-unit>
+                       <trans-unit id="log_file_path">
+                               <source>Path: Full path to the log file (may be relative to the web root, or use the EXT: syntax).</source>
+                       </trans-unit>
+                       <trans-unit id="maximum_rows">
+                               <source>Maximum number of rows: Maximum number of rows that should be stored in the log table</source>
+                       </trans-unit>
+                       <trans-unit id="optimize">
+                               <source>Optimize devlog table: Run OPTIMIZE on the tx_devlog_domain_model_entry table after records are purged to reduce the overhead. Note that this will work only with MySQL databases.</source>
+                       </trans-unit>
+                       <trans-unit id="maximum_extra_data_size">
+                               <source>Maximum size of extra data: The extra data field accepts an array containing any number of data. However when that array becomes too large, writing it to the database may actually crash you server. It is recommended to set a limit (in number of bytes).</source>
+                       </trans-unit>
+                       <trans-unit id="status_ok">
+                               <source>OK</source>
+                       </trans-unit>
+                       <trans-unit id="status_information">
+                               <source>Information</source>
+                       </trans-unit>
+                       <trans-unit id="status_notice">
+                               <source>Notice</source>
+                       </trans-unit>
+                       <trans-unit id="status_warning">
+                               <source>Warning</source>
+                       </trans-unit>
+                       <trans-unit id="status_error">
+                               <source>Error</source>
+                       </trans-unit>
+               </body>
+       </file>
+</xliff>
\ No newline at end of file
diff --git a/Resources/Private/Language/JavaScript.xlf b/Resources/Private/Language/JavaScript.xlf
new file mode 100644 (file)
index 0000000..e737e40
--- /dev/null
@@ -0,0 +1,61 @@
+<?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="severity-1" xml:space="preserve">
+                               <source>OK</source>
+                       </trans-unit>
+                       <trans-unit id="severity0" xml:space="preserve">
+                               <source>Info</source>
+                       </trans-unit>
+                       <trans-unit id="severity1" xml:space="preserve">
+                               <source>Notice</source>
+                       </trans-unit>
+                       <trans-unit id="severity2" xml:space="preserve">
+                               <source>Warning</source>
+                       </trans-unit>
+                       <trans-unit id="severity3" xml:space="preserve">
+                               <source>Error</source>
+                       </trans-unit>
+                       <trans-unit id="clear_all_entries" xml:space="preserve">
+                               <source>Clear all entries</source>
+                       </trans-unit>
+                       <trans-unit id="cleanup_for_period" xml:space="preserve">
+                               <source>Clear all entries older than</source>
+                       </trans-unit>
+                       <trans-unit id="cleanup_for_key" xml:space="preserve">
+                               <source>Clear all entries for key</source>
+                       </trans-unit>
+                       <trans-unit id="1hour" xml:space="preserve">
+                               <source>1 hour</source>
+                       </trans-unit>
+                       <trans-unit id="1day" xml:space="preserve">
+                               <source>1 day</source>
+                       </trans-unit>
+                       <trans-unit id="1week" xml:space="preserve">
+                               <source>1 week</source>
+                       </trans-unit>
+                       <trans-unit id="1month" xml:space="preserve">
+                               <source>1 month</source>
+                       </trans-unit>
+                       <trans-unit id="3months" xml:space="preserve">
+                               <source>3 months</source>
+                       </trans-unit>
+                       <trans-unit id="6months" xml:space="preserve">
+                               <source>6 months</source>
+                       </trans-unit>
+                       <trans-unit id="1year" xml:space="preserve">
+                               <source>1 year</source>
+                       </trans-unit>
+                       <trans-unit id="clear_confirm_title" xml:space="preserve">
+                               <source>Are you sure?</source>
+                       </trans-unit>
+                       <trans-unit id="clear_all_confirm" xml:space="preserve">
+                               <source>Do you really want to delete all log entries?</source>
+                       </trans-unit>
+                       <trans-unit id="clear_selected_confirm" xml:space="preserve">
+                               <source>Do you really want to delete selected log entries?</source>
+                       </trans-unit>
+               </body>
+       </file>
+</xliff>
index 3436805..bf9b031 100644 (file)
                        <trans-unit id="search_data" xml:space="preserve">
                                <source>Search log data</source>
                        </trans-unit>
-                       <trans-unit id="latest_run" xml:space="preserve">
-                               <source>latest log run</source>
-                       </trans-unit>
-                       <trans-unit id="latest_25" xml:space="preserve">
-                               <source>latest 25 entries</source>
-                       </trans-unit>
-                       <trans-unit id="latest_50" xml:space="preserve">
-                               <source>latest 50 entries</source>
-                       </trans-unit>
-                       <trans-unit id="latest_100" xml:space="preserve">
-                               <source>latest 100 entries</source>
-                       </trans-unit>
-                       <trans-unit id="all_entries" xml:space="preserve">
-                               <source>all entries</source>
-                       </trans-unit>
                        <trans-unit id="reload" xml:space="preserve">
                                <source>Reload</source>
                        </trans-unit>
                        <trans-unit id="hide_extra_data" xml:space="preserve">
                                <source>Hide extra data</source>
                        </trans-unit>
-                       <trans-unit id="severity_ok" xml:space="preserve">
-                               <source>OK</source>
-                       </trans-unit>
-                       <trans-unit id="severity_info" xml:space="preserve">
-                               <source>Info</source>
-                       </trans-unit>
-                       <trans-unit id="severity_notification" xml:space="preserve">
-                               <source>Notice</source>
-                       </trans-unit>
-                       <trans-unit id="severity_warning" xml:space="preserve">
-                               <source>Warning</source>
-                       </trans-unit>
-                       <trans-unit id="severity_error" xml:space="preserve">
-                               <source>Error</source>
-                       </trans-unit>
                        <trans-unit id="expand_all_extra_data" xml:space="preserve">
                                <source>Expand all extra data</source>
                        </trans-unit>
                        <trans-unit id="clearlog" xml:space="preserve">
                                <source>Clear Log</source>
                        </trans-unit>
-                       <trans-unit id="clearlog_intro" xml:space="preserve">
-                               <source>Use any of the clean up options below to clear your developer log.</source>
+                       <trans-unit id="clearlog_action_error" xml:space="preserve">
+                               <source>Wrong action called. No log entries were deleted.</source>
                        </trans-unit>
-                       <trans-unit id="clearalllog" xml:space="preserve">
-                               <source>Clear All Entries!</source>
+                       <trans-unit id="clearlog_invalid_period_error" xml:space="preserve">
+                               <source>Wrong period requested. No log entries were deleted.</source>
+                       </trans-unit>
+                       <trans-unit id="clearlog_nothing_cleared" xml:space="preserve">
+                               <source>No matching log entries were found and nothing was deleted.</source>
                        </trans-unit>
                        <trans-unit id="cleared_log" xml:space="preserve">
                                <source>%d log entries were deleted.</source>
                        </trans-unit>
-                       <trans-unit id="cleanup_for_period" xml:space="preserve">
-                               <source>Delete all log entries that are older than:</source>
-                       </trans-unit>
-                       <trans-unit id="cleanup_for_extension" xml:space="preserve">
-                               <source>Delete all log entries for extension:</source>
-                       </trans-unit>
-                       <trans-unit id="cleanup_all" xml:space="preserve">
-                               <source>Pressing the button below will actually delete ALL log entries. Make sure that this is really what you want to do!</source>
-                       </trans-unit>
-                       <trans-unit id="1hour" xml:space="preserve">
-                               <source>1 hour</source>
-                       </trans-unit>
-                       <trans-unit id="1week" xml:space="preserve">
-                               <source>1 week</source>
-                       </trans-unit>
-                       <trans-unit id="1month" xml:space="preserve">
-                               <source>1 month</source>
-                       </trans-unit>
-                       <trans-unit id="3months" xml:space="preserve">
-                               <source>3 months</source>
-                       </trans-unit>
-                       <trans-unit id="6months" xml:space="preserve">
-                               <source>6 months</source>
-                       </trans-unit>
-                       <trans-unit id="1year" xml:space="preserve">
-                               <source>1 year</source>
-                       </trans-unit>
                        <trans-unit id="no_entries" xml:space="preserve">
                                <source>There are currently no log entries in the Developer's Log.</source>
                        </trans-unit>
-                       <trans-unit id="status_ok">
-                               <source>OK</source>
-                       </trans-unit>
-                       <trans-unit id="status_information">
-                               <source>Information</source>
-                       </trans-unit>
-                       <trans-unit id="status_notice">
-                               <source>Notice</source>
-                       </trans-unit>
-                       <trans-unit id="status_warning">
-                               <source>Warning</source>
-                       </trans-unit>
-                       <trans-unit id="status_error">
-                               <source>Error</source>
-                       </trans-unit>
                </body>
        </file>
 </xliff>
\ No newline at end of file
diff --git a/Resources/Private/Language/locallang_configuration.xlf b/Resources/Private/Language/locallang_configuration.xlf
deleted file mode 100644 (file)
index 35f3416..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
-<xliff version="1.0">
-       <file source-language="en" datatype="plaintext" original="messages" date="2014-10-15T16:49:33Z" product-name="devlog">
-               <header/>
-               <body>
-                       <trans-unit id="general">
-                               <source>General</source>
-                       </trans-unit>
-                       <trans-unit id="db_writer">
-                               <source>Database Writer</source>
-                       </trans-unit>
-                       <trans-unit id="file_writer">
-                               <source>File Writer</source>
-                       </trans-unit>
-                       <trans-unit id="limits">
-                               <source>Limits</source>
-                       </trans-unit>
-                       <trans-unit id="filtering">
-                               <source>Filtering</source>
-                       </trans-unit>
-                       <trans-unit id="display">
-                               <source>Display</source>
-                       </trans-unit>
-                       <trans-unit id="minimum_level">
-                               <source>Minimum level for logging: Minimum message level required for actually writing to the logs.</source>
-                       </trans-unit>
-                       <trans-unit id="excluded_keys">
-                               <source>Excluded keys: Comma-separated list of (extension) keys whose entries should not be written to the logs.</source>
-                       </trans-unit>
-                       <trans-unit id="ip_filter">
-                               <source>Restricted IP addresses: Only requests coming from the given IP addresses will be logged. An empty value will block everything, a "*" will allow everything. Use "devIPmask" (not case-sensitive) to reuse devIPmask value.</source>
-                       </trans-unit>
-                       <trans-unit id="log_file_path">
-                               <source>Path: Full path to the log file (may be relative to the web root, or use the EXT: syntax).</source>
-                       </trans-unit>
-                       <trans-unit id="maximum_rows">
-                               <source>Maximum number of rows: Maximum number of rows that should be stored in the log table</source>
-                       </trans-unit>
-                       <trans-unit id="optimize">
-                               <source>Optimize devlog table: Run OPTIMIZE on the tx_devlog_domain_model_entry table after records are purged to reduce the overhead. Note that this will work only with MySQL databases.</source>
-                       </trans-unit>
-                       <trans-unit id="maximum_extra_data_size">
-                               <source>Maximum size of extra data: The extra data field accepts an array containing any number of data. However when that array becomes too large, writing it to the database may actually crash you server. It is recommended to set a limit (in number of bytes).</source>
-                       </trans-unit>
-               </body>
-       </file>
-</xliff>
\ No newline at end of file
index 5528f76..ce51d21 100644 (file)
@@ -6,6 +6,11 @@
        <h1><f:translate id="title" /></h1>
 
        <div id="tx_devlog_list_wrapper" class="hidden">
+               <!-- Hidden form triggered by clear log menu -->
+               <f:form name="delete" id="tx_devlog_delete" action="delete" method="post">
+                       <f:form.hidden name="clear" id="tx_devlog_delete_clear" value="" />
+                       <f:form.hidden name="value" id="tx_devlog_delete_value" value="" />
+               </f:form>
                <form name="reload" class="form-inline">
                        <div class="form-row">
                                <div class="form-group">
index a3b6059..400f537 100644 (file)
@@ -29,11 +29,12 @@ require.config({
 define(['jquery',
                'moment',
                'TYPO3/CMS/Backend/Icons',
+               'TYPO3/CMS/Backend/Modal',
                'datatables.net',
                'TYPO3/CMS/Backend/jquery.clearable',
                './jquery.mark.min',
                './datatables.mark.min'
-          ], function($, moment, Icons) {
+          ], function($, moment, Icons, Modal) {
        'use strict';
 
        var DevlogListModule = {
@@ -207,6 +208,7 @@ define(['jquery',
                                                DevlogListModule.initializeExtraDataToggle();
                                                DevlogListModule.initializeReloadControls();
                                                DevlogListModule.initializeFilters();
+                                               DevlogListModule.initializeClearLogMenu();
                                                DevlogListModule.toggleLoadingMask();
                                        }
                                });
@@ -435,11 +437,120 @@ define(['jquery',
                                DevlogListModule.toggleLoadingMask();
                                // Reload the filters (there may new values to insert into the selectors)
                                DevlogListModule.initializeFilters($('#tx_devlog_list'));
+                               // Update the clear log menu (items and numbers may have changed)
+                               DevlogListModule.initializeClearLogMenu();
                        }
                });
        };
 
        /**
+        * Populates the clear log menu with relevant entries and activates
+        * a delete "reaction".
+        */
+       DevlogListModule.initializeClearLogMenu = function      () {
+               var clearLogMenu = $('select[name="_devlogClearMenu"]');
+               // Disable the menu until it has finished loading
+               clearLogMenu.attr('disabled', true);
+               // Remove the "onchange" attribute that was automatically added to the JumpMenu
+               // (in this case, we don't want the selector to behave as a JumpMenu)
+               // This is kind of hacky, but it seemed simpler than trying to introduce a new menu type in the docheader.
+               clearLogMenu.removeAttr('onchange', '');
+               // Remove existing dynamic options
+               clearLogMenu.find('.devlog-item').remove();
+               clearLogMenu.find('optgroup').remove();
+               // Get count of items
+               $.ajax({
+                       url: TYPO3.settings.ajaxUrls['tx_devlog_count'],
+                       data: {
+                               timestamp: DevlogListModule.lastUpdateTime
+                       },
+                       success: function (data, status, xhr) {
+                               // Rebuild new options (if there's anything to clear)
+                               if (data.all > 0) {
+                                       // Clear all option
+                                       var optionAll = DevlogListModule.buildClearLogMenuOption(TYPO3.lang['clear_all_entries'], data.all, 'all');
+                                       clearLogMenu.append($(optionAll));
+
+                                       var optionGroup = '';
+                                       // Clear by age option group
+                                       var options = '';
+                                       for (var period in data.periods) {
+                                               if (data.periods.hasOwnProperty(period) && data.periods[period] > 0) {
+                                                       options += DevlogListModule.buildClearLogMenuOption(TYPO3.lang[period], data.periods[period], 'period', period);
+                                               }
+                                       }
+                                       if (options !== '') {
+                                               optionGroup = '<optgroup label="' + TYPO3.lang['cleanup_for_period'] + '">';
+                                               optionGroup += options;
+                                               optionGroup += '</optgroup>';
+                                               clearLogMenu.append($(optionGroup));
+                                       }
+
+                                       // Clear by key option group
+                                       optionGroup = '<optgroup label="' + TYPO3.lang['cleanup_for_key'] + '">';
+                                       for (var key in data.keys) {
+                                               if (data.keys.hasOwnProperty(key)) {
+                                                       optionGroup += DevlogListModule.buildClearLogMenuOption(key, data.keys[key], 'key', key);
+                                               }
+                                       }
+                                       optionGroup += '</optgroup>';
+                                       clearLogMenu.append($(optionGroup));
+
+                                       // Enable the menu again
+                                       clearLogMenu.attr('disabled', false);
+                               }
+                       },
+                       complete: function (xhr, status) {
+                               // Activate menu
+                               clearLogMenu.on('change', function() {
+                                       var selectedItem = $(this).find(':selected');
+                                       // Act only if the item has a data-action attribute
+                                       var action = selectedItem.data('action');
+                                       if (action) {
+                                               // Display a confirmation modal dialog box
+                                               var confirmTitle = TYPO3.lang['clear_confirm_title'];
+                                               var confirmMessage = TYPO3.lang['clear_selected_confirm'];
+                                               if (action === 'all') {
+                                                       confirmMessage = TYPO3.lang['clear_all_confirm'];
+                                               }
+                                               var confirmDialog = Modal.confirm(confirmTitle, confirmMessage);
+                                               // On cancel, simply dismiss the modal
+                                               confirmDialog.on('confirm.button.cancel', function() {
+                                                       Modal.currentModal.trigger('modal-dismiss');
+                                               });
+                                               // On confirm, set relevant values in hidden form and submit
+                                               confirmDialog.on('confirm.button.ok', function() {
+                                                       Modal.currentModal.trigger('modal-dismiss');
+                                                       $('#tx_devlog_delete_clear').val(action);
+                                                       $('#tx_devlog_delete_value').val(selectedItem.data('value'));
+                                                       $('#tx_devlog_delete').submit();
+                                               });
+                                       }
+                               });
+                       }
+               });
+       };
+
+       /**
+        * Renders a single menu item.
+        *
+        * @param text
+        * @param count
+        * @param action
+        * @param value
+        * @returns {string}
+        */
+       DevlogListModule.buildClearLogMenuOption = function (text, count, action, value) {
+               var option = '<option class="devlog-item" data-action="' + action + '"';
+               if (typeof value !== 'undefined') {
+                       option += 'data-value="' + value + '"';
+               }
+               option += '>' + text + ' (' + count + ')';
+               option += '</option>';
+               return option;
+       };
+
+       /**
         * Toggles visibility of loading mask and table view.
         *
         * Also toggles visibility of the "no entries" message.
index b91b5b4..d388145 100644 (file)
@@ -66,7 +66,7 @@ class EntryRepositoryTest extends \Tx_Phpunit_Database_TestCase
 
         $records = $this->subject->findAll();
         self::assertCount(
-                2,
+                3,
                 $records
         );
     }
@@ -85,4 +85,255 @@ class EntryRepositoryTest extends \Tx_Phpunit_Database_TestCase
                 $records
         );
     }
+
+    /**
+     * @test
+     */
+    public function findByKeyReturnsRelatedRecords() {
+        $this->importDataSet(__DIR__ . '/Fixtures/DevlogEntries.xml');
+
+        // Find entries for key "foo"
+        $records = $this->subject->findByKey('foo');
+        self::assertCount(
+                2,
+                $records
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function countByKeyReturnsEmptyArrayForEmptyDatabase()
+    {
+        $records = $this->subject->countByKey();
+        self::assertCount(
+                0,
+                $records
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function countByKeyReturnsKeyCount()
+    {
+        $this->importDataSet(__DIR__ . '/Fixtures/DevlogEntries.xml');
+
+        $records = $this->subject->countByKey();
+        self::assertSame(
+                array(
+                        'bar' => 1,
+                        'foo' => 2
+                ),
+                $records
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function countByPeriodReturnsEmptyArrayForEmptyDatabase()
+    {
+        $records = $this->subject->countByPeriod();
+        self::assertSame(
+                array(
+                        '1hour' => 0,
+                        '1day' => 0,
+                        '1week' => 0,
+                        '1month' => 0,
+                        '3months' => 0,
+                        '6months' => 0,
+                        '1year' => 0
+                ),
+                $records
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function countByPeriodReturnsPeriodCount()
+    {
+        $this->importDataSet(__DIR__ . '/Fixtures/DevlogEntries.xml');
+        $this->adjustEntriesAge();
+
+        $records = $this->subject->countByPeriod();
+        self::assertSame(
+                array(
+                        '1hour' => 3,
+                        '1day' => 2,
+                        '1week' => 2,
+                        '1month' => 1,
+                        '3months' => 1,
+                        '6months' => 0,
+                        '1year' => 0
+                ),
+                $records
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function deleteAllDeletesAllEntries() {
+        $this->importDataSet(__DIR__ . '/Fixtures/DevlogEntries.xml');
+
+        $deleted = $this->subject->deleteAll();
+        $records = $this->subject->findAll();
+        // Asset that the expected number of records were deleted and that there are none left in the database
+        self::assertSame(
+                3,
+                $deleted
+        );
+        self::assertCount(
+                0,
+                $records
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function deleteByKeyDeletesEntriesForKey() {
+        $this->importDataSet(__DIR__ . '/Fixtures/DevlogEntries.xml');
+
+        $deleted = $this->subject->deleteByKey('foo');
+        $records = $this->subject->findByKey('foo');
+        // Asset that the expected number of records were deleted and that there are none left with this key in the database
+        self::assertSame(
+                2,
+                $deleted
+        );
+        self::assertCount(
+                0,
+                $records
+        );
+    }
+
+    /**
+     * Provides various periods for deleting by period.
+     *
+     * @return array
+     */
+    public function periodProvider()
+    {
+        return array(
+                'Nothing deleted for 1 year age' => array(
+                        EntryRepository::PERIOD_1YEAR,
+                        0,
+                        array(
+                                '1hour' => 3,
+                                '1day' => 2,
+                                '1week' => 2,
+                                '1month' => 1,
+                                '3months' => 1,
+                                '6months' => 0,
+                                '1year' => 0
+                        ),
+                ),
+                'Two records deleted for 1 week age' => array(
+                        EntryRepository::PERIOD_1WEEK,
+                        2,
+                        array(
+                                '1hour' => 1,
+                                '1day' => 0,
+                                '1week' => 0,
+                                '1month' => 0,
+                                '3months' => 0,
+                                '6months' => 0,
+                                '1year' => 0
+                        ),
+                ),
+                'One record deleted for 1 month age' => array(
+                        EntryRepository::PERIOD_1MONTH,
+                        1,
+                        array(
+                                '1hour' => 2,
+                                '1day' => 1,
+                                '1week' => 1,
+                                '1month' => 0,
+                                '3months' => 0,
+                                '6months' => 0,
+                                '1year' => 0
+                        ),
+                ),
+                'All records deleted for 1 hour age' => array(
+                        EntryRepository::PERIOD_1HOUR,
+                        3,
+                        array(
+                                '1hour' => 0,
+                                '1day' => 0,
+                                '1week' => 0,
+                                '1month' => 0,
+                                '3months' => 0,
+                                '6months' => 0,
+                                '1year' => 0
+                        ),
+                ),
+        );
+    }
+
+    /**
+     * @param int $period Age for records to delete
+     * @param int $deleted Expected number of deleted records
+     * @param array $count Expected record count after deletion
+     * @test
+     * @dataProvider periodProvider
+     */
+    public function deleteByPeriodDeletesEntriesForPeriod($period, $deleted, $count)
+    {
+        $this->importDataSet(__DIR__ . '/Fixtures/DevlogEntries.xml');
+        $this->adjustEntriesAge();
+
+        $deletedRecords = $this->subject->deleteByPeriod($period);
+        $records = $this->subject->countByPeriod();
+        // Asset that the expected number of records were deleted and that the count by period is as expected
+        self::assertSame(
+                $deleted,
+                $deletedRecords
+        );
+        self::assertSame(
+                $count,
+                $records
+        );
+    }
+
+    /**
+     * Adjusts age of fixture log entries.
+     *
+     * For the tests acting on periods, we can't work with the fixtures as is
+     * because they grow older over time. Hence we dynamically change
+     * the "crdate" values in the test database to have records with known
+     * age intervals.
+     *
+     * @return void
+     */
+    protected function adjustEntriesAge()
+    {
+        $db = \Tx_Phpunit_Service_Database::getDatabaseConnection();
+        // Make sure the first entry is at least 3 months old (but not more than 6)
+        $db->exec_UPDATEquery(
+                'tx_devlog_domain_model_entry',
+                'uid = 1',
+                array(
+                        'crdate' => time() - EntryRepository::PERIOD_3MONTHS - 3600
+                )
+        );
+        // Make sure the second record is at least 1 day old (but not much older)
+        $db->exec_UPDATEquery(
+                'tx_devlog_domain_model_entry',
+                'uid = 2',
+                array(
+                        'crdate' => time() - EntryRepository::PERIOD_1WEEK - 3600
+                )
+        );
+        // Make sure the third record is at least 1 hour old (but not much older)
+        $db->exec_UPDATEquery(
+                'tx_devlog_domain_model_entry',
+                'uid = 3',
+                array(
+                        'crdate' => time() - EntryRepository::PERIOD_1HOUR - 10
+                )
+        );
+    }
 }
\ No newline at end of file
index e26dbc9..c876079 100644 (file)
@@ -7,7 +7,7 @@
                <run_id>14677290780.15759200</run_id>
                <sorting>1</sorting>
                <severity>0</severity>
-               <extkey>devlog</extkey>
+               <extkey>foo</extkey>
                <message>This is a fixture entry</message>
                <extra_data></extra_data>
                <location>DevlongEntries.xml</location>
                <cruser_id>1</cruser_id>
        </tx_devlog_domain_model_entry>
 
-       <!-- "new" entry from July 31, 2016 -->
+       <!-- another "old" entry -->
        <tx_devlog_domain_model_entry>
                <uid>2</uid>
                <pid>0</pid>
+               <run_id>14677290780.15759200</run_id>
+               <sorting>2</sorting>
+               <severity>0</severity>
+               <extkey>foo</extkey>
+               <message>This is a fixture entry</message>
+               <extra_data></extra_data>
+               <location>DevlongEntries.xml</location>
+               <line>2</line>
+               <ip>::1</ip>
+               <crdate>1467729080</crdate>
+               <cruser_id>1</cruser_id>
+       </tx_devlog_domain_model_entry>
+
+       <!-- "new" entry from July 31, 2016 -->
+       <tx_devlog_domain_model_entry>
+               <uid>3</uid>
+               <pid>0</pid>
                <run_id>14699620710.15759200</run_id>
                <sorting>1</sorting>
                <severity>1</severity>
-               <extkey>devlog</extkey>
+               <extkey>bar</extkey>
                <message>This is a new fixture entry</message>
                <extra_data></extra_data>
                <location>DevlongEntries.xml</location>
index 52cc50e..67dae62 100644 (file)
@@ -1,18 +1,18 @@
-# customcategory=general=LLL:EXT:devlog/Resources/Private/Language/locallang_configuration.xlf:general
-# customcategory=dbwriter=LLL:EXT:devlog/Resources/Private/Language/locallang_configuration.xlf:db_writer
-# customcategory=filewriter=LLL:EXT:devlog/Resources/Private/Language/locallang_configuration.xlf:file_writer
+# customcategory=general=LLL:EXT:devlog/Resources/Private/Language/Configuration.xlf:general
+# customcategory=dbwriter=LLL:EXT:devlog/Resources/Private/Language/Configuration.xlf:db_writer
+# customcategory=filewriter=LLL:EXT:devlog/Resources/Private/Language/Configuration.xlf:file_writer
 
-# customsubcategory=limits=LLL:EXT:devlog/Resources/Private/Language/locallang_configuration.xlf:limits
-# customsubcategory=filtering=LLL:EXT:devlog/Resources/Private/Language/locallang_configuration.xlf:filtering
-# customsubcategory=display=LLL:EXT:devlog/Resources/Private/Language/locallang_configuration.xlf:display
+# customsubcategory=limits=LLL:EXT:devlog/Resources/Private/Language/Configuration.xlf:limits
+# customsubcategory=filtering=LLL:EXT:devlog/Resources/Private/Language/Configuration.xlf:filtering
+# customsubcategory=display=LLL:EXT:devlog/Resources/Private/Language/Configuration.xlf:display
 
-# 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
+# cat=general/filtering/a; type=options[LLL:EXT:devlog/Resources/Private/Language/Configuration.xlf:status_ok=-1,LLL:EXT:devlog/Resources/Private/Language/Configuration.xlf:status_information=0,LLL:EXT:devlog/Resources/Private/Language/Configuration.xlf:status_notice=1,LLL:EXT:devlog/Resources/Private/Language/Configuration.xlf:status_warning=2,LLL:EXT:devlog/Resources/Private/Language/Configuration.xlf:status_error=3]; label=LLL:EXT:devlog/Resources/Private/Language/Configuration.xlf:minimum_level
 minimumLogLevel = -1
 
-# cat=general/filtering/b; 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/Configuration.xlf:excluded_keys
 excludeKeys =
 
-# cat=general/filtering/c; 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/Configuration.xlf:ip_filter
 ipFilter =
 
 # cat=general/display/a; type=integer; label=Autorefresh frequency: Set the number of seconds between each refresh, when using the autorefresh feature
@@ -21,14 +21,14 @@ refreshFrequency = 4
 # 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/c; 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/Configuration.xlf:maximum_rows
 maximumRows = 1000
 
-# cat=dbwriter/limits/a; 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/Configuration.xlf:optimize
 optimizeTable = 1
 
-# cat=dbwriter/limits/b; 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/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
+# cat=filewriter//; type=string; label=LLL:EXT:devlog/Resources/Private/Language/Configuration.xlf:log_file_path
 logFilePath =
index 0ae895d..e36afbd 100644 (file)
@@ -24,7 +24,7 @@ if (TYPO3_MODE === 'BE') {
             'after:BelogLog',
             array(
                 // An array holding the controller-action-combinations that are accessible
-                'ListModule' => 'index, get'
+                'ListModule' => 'index,delete'
             ),
             array(
                     'access' => 'admin',