diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..d23e48b --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,19 @@ +name: CI +on: [push] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Validate composer.json and composer.lock + run: composer validate + + - name: Install dependencies + run: composer install --prefer-dist --no-progress --no-suggest + + - name: Test CGL + run: ./vendor/bin/phpcs + + - name: Execute PHPUnit Tests + run: ./vendor/bin/phpunit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..51313d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.Build/ +/composer.lock +/vendor/ diff --git a/Classes/Dashboard/Widgets/PageViewsBar.php b/Classes/Dashboard/Widgets/PageViewsBar.php new file mode 100644 index 0000000..c713a33 --- /dev/null +++ b/Classes/Dashboard/Widgets/PageViewsBar.php @@ -0,0 +1,125 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use DanielSiepmann\Tracking\Extension; +use TYPO3\CMS\Core\Database\Connection; +use TYPO3\CMS\Core\Database\Query\QueryBuilder; +use TYPO3\CMS\Core\Utility\ArrayUtility; +use TYPO3\CMS\Dashboard\Widgets\AbstractBarChartWidget; + +class PageViewsBar extends AbstractBarChartWidget +{ + protected $title = Extension::LANGUAGE_PATH . ':dashboard.widgets.pageViewsBar.title'; + + protected $description = Extension::LANGUAGE_PATH . ':trackingdashboard.widgets.pageViewsBar.description'; + + protected $width = 2; + + protected $height = 4; + + /** + * @var QueryBuilder + */ + protected $queryBuilder; + + /** + * @var \ArrayObject + */ + private $settings; + + public function __construct( + string $identifier, + QueryBuilder $queryBuilder, + \ArrayObject $settings + ) { + parent::__construct($identifier); + + $this->queryBuilder = $queryBuilder; + $this->settings = $settings; + } + + protected function prepareChartData(): void + { + list($labels, $data) = $this->calculateDataForLastDays((int) $this->settings['periodInDays']); + + $this->chartData = [ + 'labels' => $labels, + 'datasets' => [ + [ + 'label' => $this->getLanguageService()->sL( + 'LLL:EXT:tracking/Resources/Private/Language/locallang.xlf:widgets.pageViewsBar.chart.dataSet.0' + ), + 'backgroundColor' => $this->chartColors[0], + 'border' => 0, + 'data' => $data + ] + ] + ]; + } + + protected function getPageViewsInPeriod(int $start, int $end): int + { + $constraints = [ + $this->queryBuilder->expr()->gte('crdate', $start), + $this->queryBuilder->expr()->lte('crdate', $end), + ]; + + if (count($this->settings['blackListedPages'])) { + $constraints[] = $this->queryBuilder->expr()->notIn( + 'tx_tracking_pageview.pid', + $this->queryBuilder->createNamedParameter( + $this->settings['blackListedPages'], + Connection::PARAM_INT_ARRAY + ) + ); + } + + return (int)$this->queryBuilder + ->count('*') + ->from('tx_tracking_pageview') + ->where(... $constraints) + ->execute() + ->fetchColumn(); + } + + protected function calculateDataForLastDays(int $days): array + { + $labels = []; + $data = []; + + $format = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] ?: 'Y-m-d'; + + for ($daysBefore = $days; $daysBefore >= 0; $daysBefore--) { + $labels[] = date($format, strtotime('-' . $daysBefore . ' day')); + $startPeriod = strtotime('-' . $daysBefore . ' day 0:00:00'); + $endPeriod = strtotime('-' . $daysBefore . ' day 23:59:59'); + + $data[] = $this->getPageViewsInPeriod($startPeriod, $endPeriod); + } + + return [ + $labels, + $data, + ]; + } +} diff --git a/Classes/Dashboard/Widgets/PageViewsPerPageDoughnut.php b/Classes/Dashboard/Widgets/PageViewsPerPageDoughnut.php new file mode 100644 index 0000000..7b59f68 --- /dev/null +++ b/Classes/Dashboard/Widgets/PageViewsPerPageDoughnut.php @@ -0,0 +1,121 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use DanielSiepmann\Tracking\Extension; +use Doctrine\DBAL\ParameterType; +use TYPO3\CMS\Core\Database\Connection; +use TYPO3\CMS\Core\Database\Query\QueryBuilder; +use TYPO3\CMS\Dashboard\Widgets\AbstractDoughnutChartWidget; + +class PageViewsPerPageDoughnut extends AbstractDoughnutChartWidget +{ + protected $title = Extension::LANGUAGE_PATH . ':dashboard.widgets.pageViewsPerPageDoughnut.title'; + + protected $description = Extension::LANGUAGE_PATH . ':dashboard.widgets.pageViewsPerPageDoughnut.description'; + + /** + * @var QueryBuilder + */ + protected $queryBuilder; + + /** + * @var \ArrayObject + */ + private $settings; + + public function __construct( + string $identifier, + QueryBuilder $queryBuilder, + \ArrayObject $settings + ) { + parent::__construct($identifier); + + $this->queryBuilder = $queryBuilder; + $this->settings = $settings; + } + + protected function prepareChartData(): void + { + list($labels, $data) = $this->getPageViewsPerPage((int) $this->settings['periodInDays']); + + $this->chartData = [ + 'labels' => $labels, + 'datasets' => [ + [ + 'backgroundColor' => $this->chartColors, + 'data' => $data, + ] + ], + ]; + } + + private function getPageViewsPerPage(int $days): array + { + $labels = []; + $data = []; + + $constraints = [ + $this->queryBuilder->expr()->gte('tx_tracking_pageview.crdate', strtotime('-' . $days . ' day 0:00:00')), + $this->queryBuilder->expr()->lte('tx_tracking_pageview.crdate', time()), + ]; + if (count($this->settings['blackListedPages'])) { + $constraints[] = $this->queryBuilder->expr()->notIn( + 'tx_tracking_pageview.pid', + $this->queryBuilder->createNamedParameter( + $this->settings['blackListedPages'], + Connection::PARAM_INT_ARRAY + ) + ); + } + + $result = $this->queryBuilder + ->selectLiteral('count(tx_tracking_pageview.pid) as total') + ->addSelect('pages.title', 'pages.uid') + ->from('tx_tracking_pageview') + ->leftJoin( + 'tx_tracking_pageview', + 'pages', + 'pages', + $this->queryBuilder->expr()->eq( + 'tx_tracking_pageview.pid', + $this->queryBuilder->quoteIdentifier('pages.uid') + ) + ) + ->where(... $constraints) + ->groupBy('tx_tracking_pageview.pid') + ->orderBy('total', 'desc') + ->setMaxResults(6) // Because 6 colors are defined + ->execute() + ->fetchAll(); + + foreach ($result as $row) { + $labels[] = $row['title'] . ' [' . $row['uid'] . ']'; + $data[] = $row['total']; + } + + return [ + $labels, + $data, + ]; + } +} diff --git a/Classes/Dashboard/Widgets/SettingsFactory.php b/Classes/Dashboard/Widgets/SettingsFactory.php new file mode 100644 index 0000000..65aac9d --- /dev/null +++ b/Classes/Dashboard/Widgets/SettingsFactory.php @@ -0,0 +1,48 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +class SettingsFactory +{ + private $defaults = [ + 'pageViewsBar' => [ + 'periodInDays' => 31, + 'blackListedPages' => [], + ], + 'pageViewsPerPageDoughnut' => [ + 'periodInDays' => 31, + 'blackListedPages' => [], + 'maxResults' => 6, + ], + ]; + + public function fromArray(string $widgetIdentifier, array $settings): \ArrayObject + { + $settingsToUse = $this->defaults[$widgetIdentifier] ?? []; + + ArrayUtility::mergeRecursiveWithOverrule($settingsToUse, $settings); + + return new \ArrayObject($settingsToUse); + } +} diff --git a/Classes/Domain/Model/Pageview.php b/Classes/Domain/Model/Pageview.php new file mode 100644 index 0000000..83404d4 --- /dev/null +++ b/Classes/Domain/Model/Pageview.php @@ -0,0 +1,103 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use TYPO3\CMS\Core\Site\Entity\SiteLanguage; + +class Pageview +{ + /** + * @var int + */ + private $pageUid; + + /** + * @var SiteLanguage + */ + private $language; + + /** + * @var \DateTimeImmutable + */ + private $crdate; + + /** + * @var int + */ + private $pageType; + + /** + * @var string + */ + private $url; + + /** + * @var string + */ + private $userAgent; + + public function __construct( + int $pageUid, + SiteLanguage $language, + \DateTimeImmutable $crdate, + int $pageType, + string $url, + string $userAgent + ) { + $this->pageUid = $pageUid; + $this->language = $language; + $this->crdate = $crdate; + $this->pageType = $pageType; + $this->url = $url; + $this->userAgent = $userAgent; + } + + public function getPageUid(): int + { + return $this->pageUid; + } + + public function getLanguage(): SiteLanguage + { + return $this->language; + } + + public function getCrdate(): \DateTimeImmutable + { + return $this->crdate; + } + + public function getPageType(): int + { + return $this->pageType; + } + + public function getUrl(): string + { + return $this->url; + } + + public function getUserAgent(): string + { + return $this->userAgent; + } +} diff --git a/Classes/Domain/Pageview/Factory.php b/Classes/Domain/Pageview/Factory.php new file mode 100644 index 0000000..f6a1ef3 --- /dev/null +++ b/Classes/Domain/Pageview/Factory.php @@ -0,0 +1,47 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use DanielSiepmann\Tracking\Domain\Model\Pageview; +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Routing\PageArguments; +use TYPO3\CMS\Core\Site\Entity\SiteLanguage; + +class Factory implements FromRequest +{ + public static function fromRequest(ServerRequestInterface $request): Pageview + { + return new PageView( + static::getRouting($request)->getPageId(), + $request->getAttribute('language'), + new \DateTimeImmutable(), + static::getRouting($request)->getPageType(), + (string) $request->getUri(), + $request->getHeader('User-Agent')[0] ?? '' + ); + } + + private static function getRouting(ServerRequestInterface $request): PageArguments + { + return $request->getAttribute('routing'); + } +} diff --git a/Classes/Domain/Pageview/FromRequest.php b/Classes/Domain/Pageview/FromRequest.php new file mode 100644 index 0000000..de4967e --- /dev/null +++ b/Classes/Domain/Pageview/FromRequest.php @@ -0,0 +1,30 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use DanielSiepmann\Tracking\Domain\Model\Pageview; +use Psr\Http\Message\ServerRequestInterface; + +interface FromRequest +{ + public static function fromRequest(ServerRequestInterface $request): Pageview; +} diff --git a/Classes/Domain/Repository/Pageview.php b/Classes/Domain/Repository/Pageview.php new file mode 100644 index 0000000..069d2ff --- /dev/null +++ b/Classes/Domain/Repository/Pageview.php @@ -0,0 +1,54 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use DanielSiepmann\Tracking\Domain\Model\Pageview as Model; +use TYPO3\CMS\Core\Database\Connection; + +class Pageview +{ + /** + * @var Connection + */ + private $connection; + + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + public function add(Model $pageview) + { + $this->connection->insert( + 'tx_tracking_pageview', + [ + 'pid' => $pageview->getPageUid(), + 'crdate' => $pageview->getCrdate()->format('U'), + 'tstamp' => $pageview->getCrdate()->format('U'), + 'type' => $pageview->getPageType(), + 'sys_language_uid' => $pageview->getLanguage()->getLanguageId(), + 'url' => $pageview->getUrl(), + 'user_agent' => $pageview->getUserAgent(), + ] + ); + } +} diff --git a/Classes/Extension.php b/Classes/Extension.php new file mode 100644 index 0000000..227fa52 --- /dev/null +++ b/Classes/Extension.php @@ -0,0 +1,29 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +final class Extension +{ + public const EXT_KEY = 'tracking'; + + public const LANGUAGE_PATH = 'LLL:EXT:' . self::EXT_KEY . '/Resources/Private/Language/locallang.xlf'; +} diff --git a/Classes/Middleware/Pageview.php b/Classes/Middleware/Pageview.php new file mode 100644 index 0000000..276ae07 --- /dev/null +++ b/Classes/Middleware/Pageview.php @@ -0,0 +1,77 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use DanielSiepmann\Tracking\Domain\Pageview\Factory; +use DanielSiepmann\Tracking\Domain\Repository\Pageview as Repository; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use TYPO3\CMS\Core\Context\Context; + +class Pageview implements MiddlewareInterface +{ + /** + * @var Repository + */ + private $repository; + + /** + * @var Context + */ + private $context; + + /** + * @var string + */ + private $rule = ''; + + public function __construct(Repository $repository, Context $context, string $rule) + { + $this->repository = $repository; + $this->context = $context; + $this->rule = $rule; + } + + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface { + if ($this->shouldTrack($request, $this->context)) { + $this->repository->add(Factory::fromRequest($request)); + } + + return $handler->handle($request); + } + + private function shouldTrack( + ServerRequestInterface $request, + Context $context + ): bool { + return (bool) (new ExpressionLanguage())->evaluate($this->rule, [ + 'request' => $request, + 'context' => $context, + ]); + } +} diff --git a/Configuration/Backend/DashboardWidgetGroups.php b/Configuration/Backend/DashboardWidgetGroups.php new file mode 100644 index 0000000..eaf82ca --- /dev/null +++ b/Configuration/Backend/DashboardWidgetGroups.php @@ -0,0 +1,6 @@ + [ + 'title' => 'LLL:EXT:tracking/Resources/Private/Language/locallang.xlf:dashboard.widget.group.tracking', + ], +]; diff --git a/Configuration/RequestMiddlewares.php b/Configuration/RequestMiddlewares.php new file mode 100644 index 0000000..a0d00ff --- /dev/null +++ b/Configuration/RequestMiddlewares.php @@ -0,0 +1,15 @@ + [ + 'tracking-pageview' => [ + 'target' => \DanielSiepmann\Tracking\Middleware\Pageview::class, + 'before' => [ + 'typo3/cms-frontend/content-length-headers', + ], + 'after' => [ + 'typo3/cms-frontend/shortcut-and-mountpoint-redirect', + ], + ], + ], +]; diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml new file mode 100644 index 0000000..245218d --- /dev/null +++ b/Configuration/Services.yaml @@ -0,0 +1,77 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + DanielSiepmann\Tracking\: + resource: '../Classes/*' + + # Virtual services + + DanielSiepmann\Tracking\DI\Dashboard\Widgets\Settings\PageViewsBar: + factory: + - '@DanielSiepmann\Tracking\Dashboard\Widgets\SettingsFactory' + - 'fromArray' + arguments: + $widgetIdentifier: 'pageViewsBar' + $settings: [] + DanielSiepmann\Tracking\DI\Dashboard\Widgets\Settings\PageViewsPerPageDoughnut: + factory: + - '@DanielSiepmann\Tracking\Dashboard\Widgets\SettingsFactory' + - 'fromArray' + arguments: + $widgetIdentifier: 'pageViewsPerPageDoughnut' + $settings: [] + + DanielSiepmann\Tracking\DI\DatabaseConnection\Pageview: + factory: + - '@TYPO3\CMS\Core\Database\ConnectionPool' + - 'getConnectionForTable' + arguments: + - 'tx_tracking_pageview' + DanielSiepmann\Tracking\DI\QueryBuilder\PageView: + factory: + - '@TYPO3\CMS\Core\Database\ConnectionPool' + - 'getQueryBuilderForTable' + arguments: + - 'tx_tracking_pageview' + + # Existing classes + + DanielSiepmann\Tracking\Domain\Repository\Pageview: + public: true + arguments: + - '@DanielSiepmann\Tracking\DI\DatabaseConnection\Pageview' + + DanielSiepmann\Tracking\Middleware\Pageview: + public: true + arguments: + $rule: > + not (context.getAspect("backend.user").isLoggedIn()) + and not (request.getHeader("User-Agent")[0] matches "/^Wget|TYPO3|TYPO3 linkvalidator/") + and not (request.getHeader("User-Agent")[0] matches "/Googlebot|Bingbot|bingbot|Slurp|DuckDuckBot|Baiduspider|YandexBot|Sogou|Exabot|NextCloud-News|Feedly|XING FeedReader|CCBot|SemrushBot|SEOkicks|Twitterbot|Seekport Crawler|SemanticScholarBot|curl/") + + # Dashboard Widgets + + DanielSiepmann\Tracking\Dashboard\Widgets\PageViewsBar: + class: 'DanielSiepmann\Tracking\Dashboard\Widgets\PageViewsBar' + arguments: + $identifier: 'pageViewsBar' + $queryBuilder: '@DanielSiepmann\Tracking\DI\QueryBuilder\PageView' + $settings: '@DanielSiepmann\Tracking\DI\Dashboard\Widgets\Settings\PageViewsBar' + tags: + - name: 'dashboard.widget' + identifier: 'pageViewsBar' + widgetGroups: 'tracking' + + DanielSiepmann\Tracking\Dashboard\Widgets\PageViewsPerPageDoughnut: + class: 'DanielSiepmann\Tracking\Dashboard\Widgets\PageViewsPerPageDoughnut' + arguments: + $identifier: 'pageViewsPerPageDoughnut' + $queryBuilder: '@DanielSiepmann\Tracking\DI\QueryBuilder\PageView' + $settings: '@DanielSiepmann\Tracking\DI\Dashboard\Widgets\Settings\PageViewsPerPageDoughnut' + tags: + - name: 'dashboard.widget' + identifier: 'pageViewsPerPageDoughnut' + widgetGroups: 'tracking' diff --git a/Configuration/TCA/tx_tracking_pageview.php b/Configuration/TCA/tx_tracking_pageview.php new file mode 100644 index 0000000..8efcf63 --- /dev/null +++ b/Configuration/TCA/tx_tracking_pageview.php @@ -0,0 +1,78 @@ + [ + 'label' => 'url', + 'label_alt' => 'crdate', + 'label_alt_force' => true, + 'sortby' => 'crdate DESC', + 'tstamp' => 'tstamp', + 'crdate' => 'crdate', + 'cruser_id' => 'cruser_id', + 'languageField' => 'sys_language_uid', + 'title' => 'LLL:EXT:tracking/Resources/Private/Language/locallang_tca.xlf:table.pageview', + 'searchFields' => 'uid, url' + ], + 'interface' => [ + 'always_description' => 0, + 'showRecordFieldList' => 'url, user_agent, type, sys_language_uid, crdate, tstamp, crdate, cruser_id' + ], + 'types' => [ + '0' => [ + 'showitem' => 'sys_language_uid, pid, url, user_agent, type, crdate', + ], + ], + 'columns' => [ + 'pid' => [ + 'label' => 'LLL:EXT:tracking/Resources/Private/Language/locallang_tca.xlf:table.pageview.pid', + 'config' => [ + 'type' => 'select', + 'readOnly' => true, + 'renderType' => 'selectSingle', + 'foreign_table' => 'pages', + ], + ], + 'crdate' => [ + 'label' => 'LLL:EXT:tracking/Resources/Private/Language/locallang_tca.xlf:table.pageview.crdate', + 'config' => [ + 'type' => 'input', + 'eval' => 'datetime', + ], + ], + 'sys_language_uid' => [ + 'label' => 'LLL:EXT:tracking/Resources/Private/Language/locallang_tca.xlf:table.pageview.sys_language', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'foreign_table' => 'sys_language', + 'items' => [ + ['LLL:EXT:tracking/Resources/Private/Language/locallang_tca.xlf:table.pageview.sys_language.0', 0], + ], + 'readOnly' => true, + ] + ], + 'user_agent' => [ + 'label' => 'LLL:EXT:tracking/Resources/Private/Language/locallang_tca.xlf:table.pageview.user_agent', + 'config' => [ + 'type' => 'input', + 'readOnly' => true, + ], + ], + 'type' => [ + 'label' => 'LLL:EXT:tracking/Resources/Private/Language/locallang_tca.xlf:table.pageview.type', + 'config' => [ + 'type' => 'input', + 'readOnly' => true, + 'eval' => 'int', + ], + ], + 'url' => [ + 'label' => 'LLL:EXT:tracking/Resources/Private/Language/locallang_tca.xlf:table.pageview.url', + 'config' => [ + 'readOnly' => true, + 'type' => 'input', + 'size' => 50, + 'max' => 255, + ], + ], + ], +]; diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf new file mode 100644 index 0000000..b1d2f3b --- /dev/null +++ b/Resources/Private/Language/locallang.xlf @@ -0,0 +1,26 @@ + + + +
+ + + Tracking + + + Page Views / Day + + + Displays total page views per day. + + + Total Page Views + + + Page Views / Page + + + Displays total page views per page. + + + + diff --git a/Resources/Private/Language/locallang_tca.xlf b/Resources/Private/Language/locallang_tca.xlf new file mode 100644 index 0000000..03f191e --- /dev/null +++ b/Resources/Private/Language/locallang_tca.xlf @@ -0,0 +1,32 @@ + + + +
+ + + Pageview + + + Page + + + URL + + + System language + + + Default system language + + + Date + Time + + + Pagetype + + + User agent + + + + diff --git a/Tests/Unit/Domain/Model/PageviewTest.php b/Tests/Unit/Domain/Model/PageviewTest.php new file mode 100644 index 0000000..87f5b1c --- /dev/null +++ b/Tests/Unit/Domain/Model/PageviewTest.php @@ -0,0 +1,169 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use DanielSiepmann\Tracking\Domain\Model\Pageview; +use PHPUnit\Framework\TestCase; +use TYPO3\CMS\Core\Site\Entity\SiteLanguage; + +/** + * @covers DanielSiepmann\Tracking\Domain\Model\Pageview + */ +class PageviewTest extends TestCase +{ + /** + * @test + */ + public function canBeCreated() + { + $language = $this->prophesize(SiteLanguage::class); + + $subject = new Pageview( + 0, + $language->reveal(), + new \DateTimeImmutable(), + 0, + '', + '' + ); + + static::assertInstanceOf(Pageview::class, $subject); + } + + /** + * @test + */ + public function returnsPageUid() + { + $language = $this->prophesize(SiteLanguage::class); + + $subject = new Pageview( + 500, + $language->reveal(), + new \DateTimeImmutable(), + 0, + '', + '' + ); + + static::assertSame(500, $subject->getPageUid()); + } + + /** + * @test + */ + public function returnsLanguage() + { + $language = $this->prophesize(SiteLanguage::class); + + $subject = new Pageview( + 0, + $language->reveal(), + new \DateTimeImmutable(), + 0, + '', + '' + ); + + static::assertSame($language->reveal(), $subject->getLanguage()); + } + + /** + * @test + */ + public function returnsCrdate() + { + $language = $this->prophesize(SiteLanguage::class); + $crdate = new \DateTimeImmutable(); + + $subject = new Pageview( + 0, + $language->reveal(), + $crdate, + 0, + '', + '' + ); + + static::assertSame($crdate, $subject->getCrdate()); + } + + /** + * @test + */ + public function returnsPageType() + { + $language = $this->prophesize(SiteLanguage::class); + + $subject = new Pageview( + 0, + $language->reveal(), + new \DateTimeImmutable(), + 999, + '', + '' + ); + + static::assertSame(999, $subject->getPageType()); + } + + /** + * @test + */ + public function returnsUrl() + { + $language = $this->prophesize(SiteLanguage::class); + + $subject = new Pageview( + 0, + $language->reveal(), + new \DateTimeImmutable(), + 0, + 'https://example.com/path.html', + '' + ); + + static::assertSame('https://example.com/path.html', $subject->getUrl()); + } + + /** + * @test + */ + public function returnsUserAgent() + { + $language = $this->prophesize(SiteLanguage::class); + + $subject = new Pageview( + 0, + $language->reveal(), + new \DateTimeImmutable(), + 0, + '', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0' + ); + + static::assertSame( + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0', + $subject->getUserAgent() + ); + } +} diff --git a/Tests/Unit/Domain/Pageview/FactoryTest.php b/Tests/Unit/Domain/Pageview/FactoryTest.php new file mode 100644 index 0000000..0c97d35 --- /dev/null +++ b/Tests/Unit/Domain/Pageview/FactoryTest.php @@ -0,0 +1,196 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use DanielSiepmann\Tracking\Domain\Model\Pageview; +use DanielSiepmann\Tracking\Domain\Pageview\Factory; +use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Routing\PageArguments; +use TYPO3\CMS\Core\Site\Entity\SiteLanguage; + +/** + * @covers DanielSiepmann\Tracking\Domain\Pageview\Factory + */ +class FactoryTest extends TestCase +{ + /** + * @test + */ + public function returnsPageview() + { + $routing = $this->prophesize(PageArguments::class); + $routing->getPageId()->willReturn(10); + $routing->getPageType()->willReturn(0); + + $language = $this->prophesize(SiteLanguage::class); + + $request = $this->prophesize(ServerRequestInterface::class); + $request->getAttribute('routing')->willReturn($routing->reveal()); + $request->getAttribute('language')->willReturn($language->reveal()); + $request->getUri()->willReturn(''); + $request->getHeader('User-Agent')->willReturn([]); + + $result = Factory::fromRequest($request->reveal()); + static::assertInstanceOf(Pageview::class, $result); + } + + /** + * @test + */ + public function returnedPageviewContainsUserAgent() + { + $routing = $this->prophesize(PageArguments::class); + $routing->getPageId()->willReturn(10); + $routing->getPageType()->willReturn(0); + + $language = $this->prophesize(SiteLanguage::class); + + $request = $this->prophesize(ServerRequestInterface::class); + $request->getAttribute('routing')->willReturn($routing->reveal()); + $request->getAttribute('language')->willReturn($language->reveal()); + $request->getUri()->willReturn(''); + $request->getHeader('User-Agent')->willReturn([ + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0' + ]); + + $result = Factory::fromRequest($request->reveal()); + static::assertSame( + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0', + $result->getUserAgent() + ); + } + + /** + * @test + */ + public function returnedPageviewContainsUri() + { + $routing = $this->prophesize(PageArguments::class); + $routing->getPageId()->willReturn(10); + $routing->getPageType()->willReturn(0); + + $language = $this->prophesize(SiteLanguage::class); + + $request = $this->prophesize(ServerRequestInterface::class); + $request->getAttribute('routing')->willReturn($routing->reveal()); + $request->getAttribute('language')->willReturn($language->reveal()); + $request->getUri()->willReturn('https://example.com/path?query=params&some=more#anchor'); + $request->getHeader('User-Agent')->willReturn([]); + + $result = Factory::fromRequest($request->reveal()); + static::assertSame( + 'https://example.com/path?query=params&some=more#anchor', + $result->getUrl() + ); + } + + /** + * @test + */ + public function returnedPageviewContainsPageType() + { + $routing = $this->prophesize(PageArguments::class); + $routing->getPageId()->willReturn(10); + $routing->getPageType()->willReturn(50); + + $language = $this->prophesize(SiteLanguage::class); + + $request = $this->prophesize(ServerRequestInterface::class); + $request->getAttribute('routing')->willReturn($routing->reveal()); + $request->getAttribute('language')->willReturn($language->reveal()); + $request->getUri()->willReturn(''); + $request->getHeader('User-Agent')->willReturn([]); + + $result = Factory::fromRequest($request->reveal()); + static::assertSame( + 50, + $result->getPageType() + ); + } + + /** + * @test + */ + public function returnedPageviewContainsDateTime() + { + $routing = $this->prophesize(PageArguments::class); + $routing->getPageId()->willReturn(10); + $routing->getPageType()->willReturn(0); + + $language = $this->prophesize(SiteLanguage::class); + + $request = $this->prophesize(ServerRequestInterface::class); + $request->getAttribute('routing')->willReturn($routing->reveal()); + $request->getAttribute('language')->willReturn($language->reveal()); + $request->getUri()->willReturn(''); + $request->getHeader('User-Agent')->willReturn([]); + + $result = Factory::fromRequest($request->reveal()); + static::assertInstanceOf(\DateTimeImmutable::class, $result->getCrdate()); + } + + /** + * @test + */ + public function returnedPageviewContainsLanguage() + { + $routing = $this->prophesize(PageArguments::class); + $routing->getPageId()->willReturn(10); + $routing->getPageType()->willReturn(0); + + $language = $this->prophesize(SiteLanguage::class); + + $request = $this->prophesize(ServerRequestInterface::class); + $request->getAttribute('routing')->willReturn($routing->reveal()); + $request->getAttribute('language')->willReturn($language->reveal()); + $request->getUri()->willReturn(''); + $request->getHeader('User-Agent')->willReturn([]); + + $result = Factory::fromRequest($request->reveal()); + static::assertInstanceOf(SiteLanguage::class, $result->getLanguage()); + } + + /** + * @test + */ + public function returnedPageviewContainsPageId() + { + $routing = $this->prophesize(PageArguments::class); + $routing->getPageId()->willReturn(10); + $routing->getPageType()->willReturn(0); + + $language = $this->prophesize(SiteLanguage::class); + + $request = $this->prophesize(ServerRequestInterface::class); + $request->getAttribute('routing')->willReturn($routing->reveal()); + $request->getAttribute('language')->willReturn($language->reveal()); + $request->getUri()->willReturn(''); + $request->getHeader('User-Agent')->willReturn([]); + + $result = Factory::fromRequest($request->reveal()); + static::assertSame( + 10, + $result->getPageUid() + ); + } +} diff --git a/Tests/Unit/Domain/Repository/PageviewTest.php b/Tests/Unit/Domain/Repository/PageviewTest.php new file mode 100644 index 0000000..bd07858 --- /dev/null +++ b/Tests/Unit/Domain/Repository/PageviewTest.php @@ -0,0 +1,72 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use DanielSiepmann\Tracking\Domain\Model\Pageview as Model; +use DanielSiepmann\Tracking\Domain\Repository\Pageview; +use PHPUnit\Framework\TestCase; +use TYPO3\CMS\Core\Database\Connection; +use TYPO3\CMS\Core\Site\Entity\SiteLanguage; + +/** + * @covers DanielSiepmann\Tracking\Domain\Repository\Pageview + */ +class PageviewTest extends TestCase +{ + /** + * @test + */ + public function modelCanBeAdded() + { + $connection = $this->prophesize(Connection::class); + + $dateTime = $this->prophesize(\DateTimeImmutable::class); + $dateTime->format('U')->willReturn(1582660189); + + $language = $this->prophesize(SiteLanguage::class); + $language->getLanguageId()->willReturn(2); + + $model = $this->prophesize(Model::class); + $model->getPageUid()->willReturn(10); + $model->getCrdate()->willReturn($dateTime->reveal()); + $model->getPageType()->willReturn(999); + $model->getLanguage()->willReturn($language->reveal()); + $model->getUrl()->willReturn('https://example.com/path.html'); + $model->getUserAgent()->willReturn('Mozilla/5.0 (Windows NT 10.0) Gecko/20100101 Firefox/74.0'); + + $connection->insert( + 'tx_tracking_pageview', + [ + 'pid' => 10, + 'crdate' => 1582660189, + 'tstamp' => 1582660189, + 'type' => 999, + 'sys_language_uid' => 2, + 'url' => 'https://example.com/path.html', + 'user_agent' => 'Mozilla/5.0 (Windows NT 10.0) Gecko/20100101 Firefox/74.0', + ] + )->willReturn(1)->shouldBeCalledTimes(1); + + $subject = new Pageview($connection->reveal()); + $subject->add($model->reveal()); + } +} diff --git a/Tests/Unit/Middleware/PageviewTest.php b/Tests/Unit/Middleware/PageviewTest.php new file mode 100644 index 0000000..5f88677 --- /dev/null +++ b/Tests/Unit/Middleware/PageviewTest.php @@ -0,0 +1,95 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use DanielSiepmann\Tracking\Domain\Model\Pageview as Model; +use DanielSiepmann\Tracking\Domain\Repository\Pageview as Repository; +use DanielSiepmann\Tracking\Middleware\Pageview; +use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; +use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Routing\PageArguments; +use TYPO3\CMS\Core\Site\Entity\SiteLanguage; + +/** + * @covers DanielSiepmann\Tracking\Middleware\Pageview + */ +class PageviewTest extends TestCase +{ + /** + * @test + */ + public function doesNotAddBlacklistedRequest() + { + $repository = $this->prophesize(Repository::class); + $context = $this->prophesize(Context::class); + $rule = 'false'; + + $request = $this->prophesize(ServerRequestInterface::class); + $response = $this->prophesize(ResponseInterface::class); + $handler = $this->prophesize(RequestHandlerInterface::class); + + $handler->handle($request->reveal())->willReturn($response->reveal()); + $repository->add()->shouldNotBeCalled(); + + $subject = new Pageview($repository->reveal(), $context->reveal(), $rule); + $result = $subject->process($request->reveal(), $handler->reveal()); + + static::assertInstanceOf(ResponseInterface::class, $result); + } + + /** + * @test + */ + public function addsPageviewToRepository() + { + $repository = $this->prophesize(Repository::class); + $context = $this->prophesize(Context::class); + $rule = 'true'; + + $routing = $this->prophesize(PageArguments::class); + $routing->getPageId()->willReturn(10); + $routing->getPageType()->willReturn(0); + + $language = $this->prophesize(SiteLanguage::class); + + $request = $this->prophesize(ServerRequestInterface::class); + $request->getAttribute('routing')->willReturn($routing->reveal()); + $request->getAttribute('language')->willReturn($language->reveal()); + $request->getUri()->willReturn(''); + $request->getHeader('User-Agent')->willReturn([]); + + $response = $this->prophesize(ResponseInterface::class); + $handler = $this->prophesize(RequestHandlerInterface::class); + + $handler->handle($request->reveal())->willReturn($response->reveal()); + $repository->add(Argument::type(Model::class))->shouldBeCalledtimes(1); + + $subject = new Pageview($repository->reveal(), $context->reveal(), $rule); + $result = $subject->process($request->reveal(), $handler->reveal()); + + static::assertInstanceOf(ResponseInterface::class, $result); + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a4e6c54 --- /dev/null +++ b/composer.json @@ -0,0 +1,55 @@ +{ + "name": "danielsiepmann/tracking", + "description": "Tracking for TYPO3", + "type": "typo3-cms-extension", + "license": "GPL-2.0-or-later", + "authors": [ + { + "name": "Daniel Siepmann", + "email": "coding@daniel-siepmann.de" + } + ], + "autoload": { + "psr-4": { + "DanielSiepmann\\Tracking\\": "Classes/" + } + }, + "autoload-dev": { + "psr-4": { + "DanielSiepmann\\Tracking\\Tests\\": "Tests/" + } + }, + "require": { + "doctrine/dbal": "^2.10", + "php": "^7.3.0", + "psr/http-message": "^1.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0", + "symfony/expression-language": "^5.0", + "typo3/cms-core": "^10.3.0" + }, + "suggest": { + "typo3/cms-dashboard": "To make use of provided TYPO3 widgets" + }, + "extra": { + "typo3/cms": { + "cms-package-dir": "{$vendor-dir}/typo3/cms", + "extension-key": "tracking", + "web-dir": ".Build/web" + }, + "branch-alias": { + "dev-develop": "1.0.x-dev" + } + }, + "scripts": { + "post-autoload-dump": [ + "mkdir -p .Build/web/typo3conf/ext/", + "[ -L .Build/web/typo3conf/ext/tracking ] || ln -snvf ../../../../. .Build/web/typo3conf/ext/tracking" + ] + }, + "require-dev": { + "squizlabs/php_codesniffer": "^3.5", + "phpunit/phpunit": "^9.0", + "typo3/cms-dashboard": "^10.3.0" + } +} diff --git a/ext_emconf.php b/ext_emconf.php new file mode 100644 index 0000000..3c53263 --- /dev/null +++ b/ext_emconf.php @@ -0,0 +1,23 @@ + 'Tracking', + 'description' => 'Tracks page visits in TYPO3.', + 'category' => 'fe', + 'state' => 'alpha', + 'uploadfolder' => 0, + 'createDirs' => '', + 'clearCacheOnLoad' => 0, + 'author' => 'Daniel Siepmann', + 'author_email' => 'coding@daniel-siepmann.de', + 'author_company' => '', + 'version' => '0.1.0', + 'constraints' => [ + 'depends' => [ + 'core' => '', + ], + 'conflicts' => [], + 'suggests' => [ + 'dashboard' => '', + ], + ], +]; diff --git a/ext_localconf.php b/ext_localconf.php new file mode 100644 index 0000000..cbdb66c --- /dev/null +++ b/ext_localconf.php @@ -0,0 +1,3 @@ + + + This project coding standard + + Classes/ + Tests/ + + + + + + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..4ae4c2d --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,26 @@ + + + + + Tests/Unit/ + + + + + + Classes + + + diff --git a/readme.rst b/readme.rst new file mode 100644 index 0000000..ada3a07 --- /dev/null +++ b/readme.rst @@ -0,0 +1,59 @@ +About +===== + +This extension should only demonstrate technical features of TYPO3. +It is not intended for use in production systems. + +The following features should be demonstrated: + +PSR-4 Autoloading Standard + Use `composer.json` to provide autoloading information. + Classes will be loaded when needed. No need for require statements. + +PSR-12 Extended Coding Style Guide + Current stable Coding Style Guide, applied via Coding Sniffer. + +PSR-7 HTTP Message Interface + Also known as Request Response, used to create tracking information from incoming + request. + +PSR-11 Container Interface + Also known as Dependency Injection. + Used to resolve external dependencies, e.g. foreign classes. + Existing TYPO3 factories are used to build `QueryBuilder` instances. + Also DI is "misused" to provide configuration for dashboard widgets + and tracking blacklists. + +PSR-15 HTTP Handlers + Also known as middlewares. + Used to hook into processing to create tracking information. + +PSR-14 Event Dispatcher + Not in use yet. + +EXT:dashboard + Used to visualize collected tracking information. + +Todos +===== + +#. Add command that will iterate over all DB entries and remove ones matching the black list rule. + E.g. if rule is adjusted in meanwhile. + +#. Add further widgets. + + #. Top 404 requests (Collect them to show them). + + #. Grouped by user agents. + +#. Move bot detection to another rule. + + #. Keep indexing those requests, but mark them as bot and separate them in widgets. + + #. Provide an overview of crawls as widgets. E.g. to allow fine grained robots.txt. + +#. Add information to Admin Panel. + +#. Add operating System + + #. Another Symfony Expression which returns the OS ("Ubuntu", "Macintosh", "Android", "iPhone", "Windows")