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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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")