diff --git a/Classes/Dashboard/Provider/Recordviews.php b/Classes/Dashboard/Provider/Recordviews.php new file mode 100644 index 0000000..a9a25be --- /dev/null +++ b/Classes/Dashboard/Provider/Recordviews.php @@ -0,0 +1,207 @@ + + * + * 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 Doctrine\DBAL\Statement; +use TYPO3\CMS\Core\Database\Connection; +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Database\Query\QueryBuilder; +use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction; +use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction; +use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction; +use TYPO3\CMS\Dashboard\WidgetApi; +use TYPO3\CMS\Dashboard\Widgets\ChartDataProviderInterface; + +class Recordviews implements ChartDataProviderInterface +{ + /** + * @var ConnectionPool + */ + private $connectionPool; + + /** + * @var QueryBuilder + */ + private $queryBuilder; + + /** + * @var int + */ + private $days; + + /** + * @var int + */ + private $maxResults; + + /** + * @var array + */ + private $pagesToExclude; + + /** + * @var array + */ + private $recordTableLimitation; + + /** + * @var array + */ + private $recordTypeLimitation; + + public function __construct( + ConnectionPool $connectionPool, + QueryBuilder $queryBuilder, + int $days = 31, + int $maxResults = 6, + array $pagesToExclude = [], + array $recordTableLimitation = [], + array $recordTypeLimitation = [] + ) { + $this->connectionPool = $connectionPool; + $this->queryBuilder = $queryBuilder; + $this->days = $days; + $this->pagesToExclude = $pagesToExclude; + $this->maxResults = $maxResults; + $this->recordTableLimitation = $recordTableLimitation; + $this->recordTypeLimitation = $recordTypeLimitation; + } + + public function getChartData(): array + { + list($labels, $data) = $this->getRecordviews(); + + return [ + 'labels' => $labels, + 'datasets' => [ + [ + 'backgroundColor' => WidgetApi::getDefaultChartColors(), + 'data' => $data, + ] + ], + ]; + } + + private function getRecordviews(): array + { + $labels = []; + $data = []; + + foreach ($this->getRecordviewsRecords() as $recordview) { + $record = $this->getRecord($recordview['record_uid'], $recordview['record_table_name']); + + if ( + $this->recordTypeLimitation !== [] + && in_array($record['type'], $this->recordTypeLimitation) === false + ) { + continue; + } + + $labels[] = mb_strimwidth($record['title'], 0, 25, '…'); + $data[] = $recordview['total']; + } + + return [ + $labels, + $data, + ]; + } + + private function getRecordviewsRecords(): \Generator + { + $constraints = [ + $this->queryBuilder->expr()->gte( + 'tx_tracking_recordview.crdate', + strtotime('-' . $this->days . ' day 0:00:00') + ), + $this->queryBuilder->expr()->lte( + 'tx_tracking_recordview.crdate', + time() + ), + ]; + + if (count($this->pagesToExclude)) { + $constraints[] = $this->queryBuilder->expr()->notIn( + 'tx_tracking_recordview.pid', + $this->queryBuilder->createNamedParameter( + $this->pagesToExclude, + Connection::PARAM_INT_ARRAY + ) + ); + } + + if (count($this->recordTableLimitation)) { + $constraints[] = $this->queryBuilder->expr()->in( + 'tx_tracking_recordview.record_table_name', + $this->queryBuilder->createNamedParameter( + $this->recordTableLimitation, + Connection::PARAM_STR_ARRAY + ) + ); + } + + $result = $this->queryBuilder + ->selectLiteral('count(record) as total') + ->addSelect('record_uid', 'record_table_name') + ->from('tx_tracking_recordview') + ->where(... $constraints) + ->groupBy('record') + ->orderBy('total', 'desc') + ->setMaxResults($this->maxResults) + ->execute(); + + while ($row = $result->fetch()) { + yield $row; + } + } + + private function getRecord(int $recordUid, string $recordTable): array + { + $titlefield = $GLOBALS['TCA'][$recordTable]['ctrl']['label']; + $recordTypeField = $GLOBALS['TCA'][$recordTable]['ctrl']['type'] ?? ''; + + $queryBuilder = $this->connectionPool->getQueryBuilderForTable($recordTable); + + $queryBuilder->getRestrictions() + ->removeByType(StartTimeRestriction::class) + ->removeByType(EndTimeRestriction::class) + ->removeByType(HiddenRestriction::class) + ; + + $queryBuilder->select($titlefield) + ->from($recordTable) + ->where('uid = ' . $recordUid); + + if ($recordTypeField !== '') { + $queryBuilder->addSelect($recordTypeField); + } + + $record = $queryBuilder->execute()->fetch(); + + return [ + 'title' => $record[$titlefield], + 'type' => $record[$recordTypeField] ?? '', + ]; + } +} diff --git a/Classes/Domain/Model/Extractor.php b/Classes/Domain/Model/Extractor.php new file mode 100644 index 0000000..0ec7da9 --- /dev/null +++ b/Classes/Domain/Model/Extractor.php @@ -0,0 +1,60 @@ + + * + * 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. + */ + +/** + * API to extract further info out of an model. + */ +class Extractor +{ + public static function getOperatingSystem(HasUserAgent $model): string + { + $userAgent = $model->getUserAgent(); + + if (mb_stripos($userAgent, 'Android') !== false) { + return 'Android'; + } + if (mb_stripos($userAgent, 'Windows') !== false) { + return 'Windows'; + } + if (mb_stripos($userAgent, 'Linux') !== false) { + return 'Linux'; + } + if (mb_stripos($userAgent, 'Macintosh') !== false) { + return 'Macintosh'; + } + if (mb_stripos($userAgent, 'CrOS') !== false) { + return 'Google Chrome OS'; + } + if (mb_stripos($userAgent, 'OpenBSD') !== false) { + return 'OpenBSD'; + } + if ( + mb_stripos($userAgent, 'iPad') !== false + || mb_stripos($userAgent, 'iphone') !== false + ) { + return 'iOS'; + } + + return ''; + } +} diff --git a/Classes/Domain/Pageview/FromRequest.php b/Classes/Domain/Model/HasUserAgent.php similarity index 76% rename from Classes/Domain/Pageview/FromRequest.php rename to Classes/Domain/Model/HasUserAgent.php index de4967e..eabf7a6 100644 --- a/Classes/Domain/Pageview/FromRequest.php +++ b/Classes/Domain/Model/HasUserAgent.php @@ -1,6 +1,6 @@ @@ -21,10 +21,7 @@ namespace DanielSiepmann\Tracking\Domain\Pageview; * 02110-1301, USA. */ -use DanielSiepmann\Tracking\Domain\Model\Pageview; -use Psr\Http\Message\ServerRequestInterface; - -interface FromRequest +interface HasUserAgent { - public static function fromRequest(ServerRequestInterface $request): Pageview; + public function getUserAgent(): string; } diff --git a/Classes/Domain/Model/Pageview.php b/Classes/Domain/Model/Pageview.php index 8c760bd..0a91655 100644 --- a/Classes/Domain/Model/Pageview.php +++ b/Classes/Domain/Model/Pageview.php @@ -23,7 +23,7 @@ namespace DanielSiepmann\Tracking\Domain\Model; use TYPO3\CMS\Core\Site\Entity\SiteLanguage; -class Pageview +class Pageview implements HasUserAgent { /** * @var int @@ -115,31 +115,6 @@ class Pageview public function getOperatingSystem(): string { - if (mb_stripos($this->userAgent, 'Android') !== false) { - return 'Android'; - } - if (mb_stripos($this->userAgent, 'Windows') !== false) { - return 'Windows'; - } - if (mb_stripos($this->userAgent, 'Linux') !== false) { - return 'Linux'; - } - if (mb_stripos($this->userAgent, 'Macintosh') !== false) { - return 'Macintosh'; - } - if (mb_stripos($this->userAgent, 'CrOS') !== false) { - return 'Google Chrome OS'; - } - if (mb_stripos($this->userAgent, 'OpenBSD') !== false) { - return 'OpenBSD'; - } - if ( - mb_stripos($this->userAgent, 'iPad') !== false - || mb_stripos($this->userAgent, 'iphone') !== false - ) { - return 'iOS'; - } - - return ''; + return Extractor::getOperatingSystem($this); } } diff --git a/Classes/Domain/Model/RecordRule.php b/Classes/Domain/Model/RecordRule.php new file mode 100644 index 0000000..ee05b77 --- /dev/null +++ b/Classes/Domain/Model/RecordRule.php @@ -0,0 +1,96 @@ + + * + * 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 RecordRule +{ + /** + * @var string + */ + private $identifier; + + /** + * @var string + */ + private $matches; + + /** + * @var string + */ + private $recordUid; + + /** + * @var string + */ + private $tableName; + + public function __construct( + string $identifier, + string $matches, + string $recordUid, + string $tableName + ) { + $this->identifier = $identifier; + $this->matches = $matches; + $this->recordUid = $recordUid; + $this->tableName = $tableName; + } + + public static function fromArray(array $config): self + { + return new RecordRule( + $config['identifier'], + $config['matches'], + $config['recordUid'], + $config['tableName'] + ); + } + + public static function multipleFromArray(array $configs): array + { + $rules = []; + foreach ($configs as $identifier => $config) { + $rules[] = static::fromArray(array_merge( + [ + 'identifier' => $identifier, + ], + $config + )); + } + return $rules; + } + + public function getMatchesExpression(): string + { + return $this->matches; + } + + public function getUidExpression(): string + { + return $this->recordUid; + } + + public function getTableName(): string + { + return $this->tableName; + } +} diff --git a/Classes/Domain/Model/Recordview.php b/Classes/Domain/Model/Recordview.php new file mode 100644 index 0000000..72e068b --- /dev/null +++ b/Classes/Domain/Model/Recordview.php @@ -0,0 +1,120 @@ + + * + * 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 Recordview implements HasUserAgent +{ + /** + * @var int + */ + private $pageUid; + + /** + * @var SiteLanguage + */ + private $language; + + /** + * @var \DateTimeImmutable + */ + private $crdate; + + /** + * @var string + */ + private $url; + + /** + * @var string + */ + private $userAgent; + + /** + * @var int + */ + private $recordUid; + + /** + * @var string + */ + private $tableName; + + public function __construct( + int $pageUid, + SiteLanguage $language, + \DateTimeImmutable $crdate, + string $url, + string $userAgent, + int $recordUid, + string $tableName + ) { + $this->pageUid = $pageUid; + $this->language = $language; + $this->crdate = $crdate; + $this->url = $url; + $this->userAgent = $userAgent; + $this->recordUid = $recordUid; + $this->tableName = $tableName; + } + + public function getPageUid(): int + { + return $this->pageUid; + } + + public function getLanguage(): SiteLanguage + { + return $this->language; + } + + public function getCrdate(): \DateTimeImmutable + { + return $this->crdate; + } + + public function getUrl(): string + { + return $this->url; + } + + public function getUserAgent(): string + { + return $this->userAgent; + } + + public function getRecordUid(): int + { + return $this->recordUid; + } + + public function getTableName(): string + { + return $this->tableName; + } + + public function getOperatingSystem(): string + { + return Extractor::getOperatingSystem($this); + } +} diff --git a/Classes/Domain/Pageview/Factory.php b/Classes/Domain/Pageview/Factory.php index d1e3fbb..10726eb 100644 --- a/Classes/Domain/Pageview/Factory.php +++ b/Classes/Domain/Pageview/Factory.php @@ -26,7 +26,7 @@ use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Core\Routing\PageArguments; use TYPO3\CMS\Core\Site\SiteFinder; -class Factory implements FromRequest +class Factory { /** * @var SiteFinder @@ -40,7 +40,7 @@ class Factory implements FromRequest public static function fromRequest(ServerRequestInterface $request): Pageview { - return new PageView( + return new Pageview( static::getRouting($request)->getPageId(), $request->getAttribute('language'), new \DateTimeImmutable(), @@ -52,7 +52,7 @@ class Factory implements FromRequest public function fromDbRow(array $dbRow): Pageview { - return new PageView( + return new Pageview( $dbRow['pid'], $this->siteFinder->getSiteByPageId($dbRow['pid'])->getLanguageById($dbRow['sys_language_uid']), new \DateTimeImmutable('@' . $dbRow['crdate']), diff --git a/Classes/Domain/Recordview/Factory.php b/Classes/Domain/Recordview/Factory.php new file mode 100644 index 0000000..32c68eb --- /dev/null +++ b/Classes/Domain/Recordview/Factory.php @@ -0,0 +1,57 @@ + + * + * 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\RecordRule; +use DanielSiepmann\Tracking\Domain\Model\Recordview; +use Psr\Http\Message\ServerRequestInterface; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use TYPO3\CMS\Core\Routing\PageArguments; + +class Factory +{ + public static function fromRequest( + ServerRequestInterface $request, + RecordRule $rule + ): Recordview { + // Need silent, as expression language doens't provide a way to check for array keys + $recordUid = @(new ExpressionLanguage())->evaluate( + $rule->getUidExpression(), + ['request' => $request] + ); + + return new Recordview( + static::getRouting($request)->getPageId(), + $request->getAttribute('language'), + new \DateTimeImmutable(), + (string) $request->getUri(), + $request->getHeader('User-Agent')[0] ?? '', + $recordUid, + $rule->getTableName() + ); + } + + private static function getRouting(ServerRequestInterface $request): PageArguments + { + return $request->getAttribute('routing'); + } +} diff --git a/Classes/Domain/Repository/Pageview.php b/Classes/Domain/Repository/Pageview.php index 6a50733..978fc61 100644 --- a/Classes/Domain/Repository/Pageview.php +++ b/Classes/Domain/Repository/Pageview.php @@ -23,7 +23,6 @@ namespace DanielSiepmann\Tracking\Domain\Repository; use DanielSiepmann\Tracking\Domain\Model\Pageview as Model; use DanielSiepmann\Tracking\Domain\Pageview\Factory; -use Doctrine\DBAL\Driver\Statement; use TYPO3\CMS\Core\Database\Connection; class Pageview @@ -60,10 +59,8 @@ class Pageview $queryBuilder = $this->connection->createQueryBuilder(); $pageViews = $queryBuilder->select('*')->from('tx_tracking_pageview')->execute(); - if ($pageViews instanceof Statement) { - while ($pageView = $pageViews->fetch()) { - yield $this->factory->fromDbRow($pageView); - } + while ($pageView = $pageViews->fetch()) { + yield $this->factory->fromDbRow($pageView); } } diff --git a/Classes/Domain/Repository/Recordview.php b/Classes/Domain/Repository/Recordview.php new file mode 100644 index 0000000..cded3d5 --- /dev/null +++ b/Classes/Domain/Repository/Recordview.php @@ -0,0 +1,63 @@ + + * + * 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\Recordview as Model; +use TYPO3\CMS\Core\Database\Connection; + +class Recordview +{ + /** + * @var Connection + */ + private $connection; + + public function __construct( + Connection $connection + ) { + $this->connection = $connection; + } + + public function add(Model $recordview): void + { + $this->connection->insert( + 'tx_tracking_recordview', + $this->getFieldsFromModel($recordview) + ); + } + + private function getFieldsFromModel(Model $recordview): array + { + return [ + 'pid' => $recordview->getPageUid(), + 'crdate' => $recordview->getCrdate()->format('U'), + 'tstamp' => $recordview->getCrdate()->format('U'), + 'sys_language_uid' => $recordview->getLanguage()->getLanguageId(), + 'url' => $recordview->getUrl(), + 'user_agent' => $recordview->getUserAgent(), + 'operating_system' => $recordview->getOperatingSystem(), + 'record_uid' => $recordview->getRecordUid(), + 'record_table_name' => $recordview->getTableName(), + 'record' => $recordview->getTableName() . '_' . $recordview->getRecordUid(), + ]; + } +} diff --git a/Classes/Middleware/Recordview.php b/Classes/Middleware/Recordview.php new file mode 100644 index 0000000..449b75d --- /dev/null +++ b/Classes/Middleware/Recordview.php @@ -0,0 +1,89 @@ + + * + * 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\RecordRule; +use DanielSiepmann\Tracking\Domain\Recordview\Factory; +use DanielSiepmann\Tracking\Domain\Repository\Recordview 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 Recordview implements MiddlewareInterface +{ + /** + * @var Repository + */ + private $repository; + + /** + * @var Context + */ + private $context; + + /** + * @var array + */ + private $rules = []; + + public function __construct( + Repository $repository, + Context $context, + array $rules + ) { + $this->repository = $repository; + $this->context = $context; + + $this->rules = RecordRule::multipleFromArray($rules); + } + + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface { + foreach ($this->rules as $rule) { + if ($this->shouldTrack($request, $this->context, $rule)) { + $this->repository->add(Factory::fromRequest($request, $rule)); + } + } + + return $handler->handle($request); + } + + private function shouldTrack( + ServerRequestInterface $request, + Context $context, + RecordRule $rule + ): bool { + // Need silent, as expression language doens't provide a way to check for array keys + return @(bool) (new ExpressionLanguage())->evaluate( + $rule->getMatchesExpression(), + [ + 'request' => $request, + 'context' => $context, + ] + ); + } +} diff --git a/Configuration/RequestMiddlewares.php b/Configuration/RequestMiddlewares.php index a0d00ff..57ecccd 100644 --- a/Configuration/RequestMiddlewares.php +++ b/Configuration/RequestMiddlewares.php @@ -2,7 +2,7 @@ return [ 'frontend' => [ - 'tracking-pageview' => [ + 'danielsiepmann/tracking/pageview' => [ 'target' => \DanielSiepmann\Tracking\Middleware\Pageview::class, 'before' => [ 'typo3/cms-frontend/content-length-headers', @@ -11,5 +11,14 @@ return [ 'typo3/cms-frontend/shortcut-and-mountpoint-redirect', ], ], + 'danielsiepmann/tracking/recordview' => [ + 'target' => \DanielSiepmann\Tracking\Middleware\Recordview::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 index 63937f9..2f57338 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -27,11 +27,32 @@ services: arguments: - 'tx_tracking_pageview' + dbconnection.tx_tracking_recordview: + class: 'TYPO3\CMS\Core\Database\Connection' + factory: + - '@TYPO3\CMS\Core\Database\ConnectionPool' + - 'getConnectionForTable' + arguments: + - 'tx_tracking_recordview' + + querybuilder.tx_tracking_recordview: + class: 'TYPO3\CMS\Core\Database\Query\QueryBuilder' + factory: + - '@TYPO3\CMS\Core\Database\ConnectionPool' + - 'getQueryBuilderForTable' + arguments: + - 'tx_tracking_recordview' + DanielSiepmann\Tracking\Domain\Repository\Pageview: public: true arguments: - '@dbconnection.tx_tracking_pageview' + DanielSiepmann\Tracking\Domain\Repository\Recordview: + public: true + arguments: + - '@dbconnection.tx_tracking_recordview' + DanielSiepmann\Tracking\Middleware\Pageview: public: true arguments: @@ -42,6 +63,11 @@ services: 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|ia_archiver|PaperLiBot|TrendsmapResolver|AhrefsBot|Nuzzel/") and not (request.getHeader("User-Agent")[0] matches "/mattermost|Slackbot|WhatsApp/") + DanielSiepmann\Tracking\Middleware\Recordview: + public: true + arguments: + $rules: [] + DanielSiepmann\Tracking\Command\UpdateDataCommand: tags: - name: 'console.command' diff --git a/Configuration/TCA/tx_tracking_pageview.php b/Configuration/TCA/tx_tracking_pageview.php index 47d4b87..e0ea0be 100644 --- a/Configuration/TCA/tx_tracking_pageview.php +++ b/Configuration/TCA/tx_tracking_pageview.php @@ -1,4 +1,5 @@ [ 'label' => 'url', @@ -22,7 +23,7 @@ return [ 'pid' => [ 'label' => 'LLL:EXT:tracking/Resources/Private/Language/locallang_tca.xlf:table.pageview.pid', 'config' => [ - // TODO: TYPO3 v10 does no longer allow to resolve PID relations, e.g. via select or group + // TYPO3 v10 does no longer allow to resolve PID relations, e.g. via select or group // This will break internal PID handling. 'type' => 'input', 'readOnly' => true, @@ -75,7 +76,6 @@ return [ 'readOnly' => true, 'type' => 'input', 'size' => 50, - 'max' => 255, ], ], ], diff --git a/Configuration/TCA/tx_tracking_recordview.php b/Configuration/TCA/tx_tracking_recordview.php new file mode 100644 index 0000000..a679c02 --- /dev/null +++ b/Configuration/TCA/tx_tracking_recordview.php @@ -0,0 +1,85 @@ + [ + 'label' => 'record', + 'label_alt' => 'crdate', + 'label_alt_force' => true, + 'default_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.recordview', + 'searchFields' => 'uid, url', + 'iconfile' => 'EXT:core/Resources/Public/Icons/T3Icons/apps/apps-pagetree-page-default.svg', + ], + 'types' => [ + '0' => [ + 'showitem' => 'sys_language_uid, pid, record, url, user_agent, operating_system, crdate', + ], + ], + 'columns' => [ + 'pid' => [ + 'label' => 'LLL:EXT:tracking/Resources/Private/Language/locallang_tca.xlf:table.recordview.pid', + 'config' => [ + // TYPO3 v10 does no longer allow to resolve PID relations, e.g. via select or group + // This will break internal PID handling. + 'type' => 'input', + 'readOnly' => true, + ], + ], + 'crdate' => [ + 'label' => 'LLL:EXT:tracking/Resources/Private/Language/locallang_tca.xlf:table.recordview.crdate', + 'config' => [ + 'type' => 'input', + 'eval' => 'datetime', + ], + ], + 'sys_language_uid' => [ + 'label' => 'LLL:EXT:tracking/Resources/Private/Language/locallang_tca.xlf:table.recordview.sys_language', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'foreign_table' => 'sys_language', + 'items' => [ + ['LLL:EXT:tracking/Resources/Private/Language/locallang_tca.xlf:table.recordview.sys_language.0', 0], + ], + 'readOnly' => true, + ] + ], + 'user_agent' => [ + 'label' => 'LLL:EXT:tracking/Resources/Private/Language/locallang_tca.xlf:table.recordview.user_agent', + 'config' => [ + 'type' => 'input', + 'readOnly' => true, + ], + ], + 'operating_system' => [ + 'label' => 'LLL:EXT:tracking/Resources/Private/Language/locallang_tca.xlf:table.recordview.operating_system', + 'config' => [ + 'type' => 'input', + 'readOnly' => true, + ], + ], + 'url' => [ + 'label' => 'LLL:EXT:tracking/Resources/Private/Language/locallang_tca.xlf:table.recordview.url', + 'config' => [ + 'readOnly' => true, + 'type' => 'input', + 'size' => 50, + ], + ], + 'record' => [ + 'label' => 'LLL:EXT:tracking/Resources/Private/Language/locallang_tca.xlf:table.recordview.record', + 'config' => [ + 'type' => 'group', + 'allowed' => 'tt_content,sys_category,pages', + 'internal_type' => 'db', + 'maxitems' => 1, + 'minitems' => 1, + 'size' => 1, + ], + ], + ], +]; diff --git a/Resources/Private/Language/locallang_tca.xlf b/Resources/Private/Language/locallang_tca.xlf index 68f515f..64418c4 100644 --- a/Resources/Private/Language/locallang_tca.xlf +++ b/Resources/Private/Language/locallang_tca.xlf @@ -30,6 +30,34 @@ Operating System + + + Recordview + + + Page + + + Record + + + URL + + + System language + + + Default system language + + + Date + Time + + + User agent + + + Operating System + diff --git a/Tests/Unit/Domain/Model/ExtractorTest.php b/Tests/Unit/Domain/Model/ExtractorTest.php new file mode 100644 index 0000000..39bba26 --- /dev/null +++ b/Tests/Unit/Domain/Model/ExtractorTest.php @@ -0,0 +1,97 @@ + + * + * 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\Extractor; +use DanielSiepmann\Tracking\Domain\Model\HasUserAgent; +use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; + +/** + * @covers DanielSiepmann\Tracking\Domain\Model\Extractor + */ +class ExtractorTest extends TestCase +{ + use ProphecyTrait; + + /** + * @test + * @dataProvider possibleUserStringWithOperatingSystems + * @testdox Operating system $expectedOperatingSystem is extracted from UserAgent string: $userAgent + */ + public function returnsOperatingSystem(string $userAgent, string $expectedOperatingSystem): void + { + $model = $this->prophesize(HasUserAgent::class); + $model->getUserAgent()->willReturn($userAgent); + + static::assertSame( + $expectedOperatingSystem, + Extractor::getOperatingSystem($model->reveal()) + ); + } + + public function possibleUserStringWithOperatingSystems(): array + { + return [ + [ + 'userAgent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.116 Safari/537.36', + 'expectedOperatingSystem' => 'Linux', + ], + [ + 'userAgent' => 'Dalvik/2.1.0 (Linux; U; Android 9; ONEPLUS A3003 Build/PKQ1.181203.001)', + 'expectedOperatingSystem' => 'Android', + ], + [ + 'userAgent' => 'Apache-HttpClient/4.5.2 (Java/1.8.0_151)', + 'expectedOperatingSystem' => '', + ], + [ + 'userAgent' => 'AwarioSmartBot/1.0 (+https://awario.com/bots.html; bots@awario.com)', + 'expectedOperatingSystem' => '', + ], + [ + 'userAgent' => 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)', + 'expectedOperatingSystem' => 'Windows', + ], + [ + 'userAgent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:73.0) Gecko/20100101 Firefox/73.0', + 'expectedOperatingSystem' => 'Macintosh', + ], + [ + 'userAgent' => 'Mozilla/5.0 (X11; CrOS x86_64 12607.82.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.123 Safari/537.36', + 'expectedOperatingSystem' => 'Google Chrome OS', + ], + [ + 'userAgent' => 'Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.8.1.4) Gecko/20070704 Firefox/52.0', + 'expectedOperatingSystem' => 'OpenBSD', + ], + [ + 'userAgent' => 'Mozilla/5.0 (iPad; CPU OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/80.0.3987.95 Mobile/15E148 Safari/604.1', + 'expectedOperatingSystem' => 'iOS', + ], + [ + 'userAgent' => 'Mozilla/5.0 (iPhone; CPU OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/22.0 Mobile/15E148 Safari/605.1.15', + 'expectedOperatingSystem' => 'iOS', + ], + ]; + } +} diff --git a/Tests/Unit/Domain/Model/PageviewTest.php b/Tests/Unit/Domain/Model/PageviewTest.php index 5c9f066..1dfa9ff 100644 --- a/Tests/Unit/Domain/Model/PageviewTest.php +++ b/Tests/Unit/Domain/Model/PageviewTest.php @@ -217,10 +217,8 @@ class PageviewTest extends TestCase /** * @test - * @dataProvider possibleUserStringWithOperatingSystems - * @testdox Operating system $expectedOperatingSystem is extracted from UserAgent string: $userAgent */ - public function returnsOperatingSystem(string $userAgent, string $expectedOperatingSystem): void + public function returnsOperatingSystem(): void { $language = $this->prophesize(SiteLanguage::class); @@ -230,58 +228,12 @@ class PageviewTest extends TestCase new \DateTimeImmutable(), 0, '', - $userAgent + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.116 Safari/537.36' ); static::assertSame( - $expectedOperatingSystem, + 'Linux', $subject->getOperatingSystem() ); } - - public function possibleUserStringWithOperatingSystems(): array - { - return [ - [ - 'userAgent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.116 Safari/537.36', - 'expectedOperatingSystem' => 'Linux', - ], - [ - 'userAgent' => 'Dalvik/2.1.0 (Linux; U; Android 9; ONEPLUS A3003 Build/PKQ1.181203.001)', - 'expectedOperatingSystem' => 'Android', - ], - [ - 'userAgent' => 'Apache-HttpClient/4.5.2 (Java/1.8.0_151)', - 'expectedOperatingSystem' => '', - ], - [ - 'userAgent' => 'AwarioSmartBot/1.0 (+https://awario.com/bots.html; bots@awario.com)', - 'expectedOperatingSystem' => '', - ], - [ - 'userAgent' => 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)', - 'expectedOperatingSystem' => 'Windows', - ], - [ - 'userAgent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:73.0) Gecko/20100101 Firefox/73.0', - 'expectedOperatingSystem' => 'Macintosh', - ], - [ - 'userAgent' => 'Mozilla/5.0 (X11; CrOS x86_64 12607.82.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.123 Safari/537.36', - 'expectedOperatingSystem' => 'Google Chrome OS', - ], - [ - 'userAgent' => 'Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.8.1.4) Gecko/20070704 Firefox/52.0', - 'expectedOperatingSystem' => 'OpenBSD', - ], - [ - 'userAgent' => 'Mozilla/5.0 (iPad; CPU OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/80.0.3987.95 Mobile/15E148 Safari/604.1', - 'expectedOperatingSystem' => 'iOS', - ], - [ - 'userAgent' => 'Mozilla/5.0 (iPhone; CPU OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/22.0 Mobile/15E148 Safari/605.1.15', - 'expectedOperatingSystem' => 'iOS', - ], - ]; - } } diff --git a/Tests/Unit/Domain/Model/RecordRuleTest.php b/Tests/Unit/Domain/Model/RecordRuleTest.php new file mode 100644 index 0000000..3c2169a --- /dev/null +++ b/Tests/Unit/Domain/Model/RecordRuleTest.php @@ -0,0 +1,129 @@ + + * + * 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\RecordRule; +use PHPUnit\Framework\TestCase; + +/** + * @covers DanielSiepmann\Tracking\Domain\Model\RecordRule + */ +class RecordRuleTest extends TestCase +{ + /** + * @test + */ + public function canBeCreatedViaConstructor(): void + { + $subject = new RecordRule( + '', + '', + '', + '' + ); + + static::assertInstanceOf(RecordRule::class, $subject); + } + + /** + * @test + */ + public function canBeCreatedFromArray(): void + { + $subject = RecordRule::fromArray([ + 'identifier' => '', + 'matches' => '', + 'recordUid' => '', + 'tableName' => '', + ]); + + static::assertInstanceOf(RecordRule::class, $subject); + } + + /** + * @test + */ + public function multipleCanBeCratedFromArray(): void + { + $result = RecordRule::multipleFromArray([ + 'identifier1' => [ + 'matches' => '', + 'recordUid' => '', + 'tableName' => '', + ], + 'identifier2' => [ + 'matches' => '', + 'recordUid' => '', + 'tableName' => '', + ], + ]); + + static::assertCount(2, $result); + static::assertInstanceOf(RecordRule::class, $result[0]); + static::assertInstanceOf(RecordRule::class, $result[1]); + } + + /** + * @test + */ + public function returnsMatchExpression(): void + { + $subject = new RecordRule( + '', + 'match expression', + '', + '' + ); + + static::assertSame('match expression', $subject->getMatchesExpression()); + } + + /** + * @test + */ + public function returnsUidExpression(): void + { + $subject = new RecordRule( + '', + '', + 'match expression', + '' + ); + + static::assertSame('match expression', $subject->getUidExpression()); + } + + /** + * @test + */ + public function returnsTableName(): void + { + $subject = new RecordRule( + '', + '', + '', + 'table_name' + ); + + static::assertSame('table_name', $subject->getTableName()); + } +} diff --git a/Tests/Unit/Domain/Model/RecordviewTest.php b/Tests/Unit/Domain/Model/RecordviewTest.php new file mode 100644 index 0000000..5b6671c --- /dev/null +++ b/Tests/Unit/Domain/Model/RecordviewTest.php @@ -0,0 +1,228 @@ + + * + * 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\Recordview; +use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; +use TYPO3\CMS\Core\Site\Entity\SiteLanguage; + +/** + * @covers DanielSiepmann\Tracking\Domain\Model\Recordview + */ +class RecordviewTest extends TestCase +{ + use ProphecyTrait; + + /** + * @test + */ + public function canBeCreated(): void + { + $language = $this->prophesize(SiteLanguage::class); + + $subject = new Recordview( + 0, + $language->reveal(), + new \DateTimeImmutable(), + '', + '', + 10, + 'sys_category' + ); + + static::assertInstanceOf(Recordview::class, $subject); + } + + /** + * @test + */ + public function returnsPageUid(): void + { + $language = $this->prophesize(SiteLanguage::class); + + $subject = new Recordview( + 500, + $language->reveal(), + new \DateTimeImmutable(), + '', + '', + 10, + 'sys_category' + ); + + static::assertSame(500, $subject->getPageUid()); + } + + /** + * @test + */ + public function returnsLanguage(): void + { + $language = $this->prophesize(SiteLanguage::class); + + $subject = new Recordview( + 0, + $language->reveal(), + new \DateTimeImmutable(), + '', + '', + 10, + 'sys_category' + ); + + static::assertSame($language->reveal(), $subject->getLanguage()); + } + + /** + * @test + */ + public function returnsCrdate(): void + { + $language = $this->prophesize(SiteLanguage::class); + $crdate = new \DateTimeImmutable(); + + $subject = new Recordview( + 0, + $language->reveal(), + $crdate, + '', + '', + 10, + 'sys_category' + ); + + static::assertSame($crdate, $subject->getCrdate()); + } + + /** + * @test + */ + public function returnsUrl(): void + { + $language = $this->prophesize(SiteLanguage::class); + + $subject = new Recordview( + 0, + $language->reveal(), + new \DateTimeImmutable(), + 'https://example.com/path.html', + '', + 10, + 'sys_category' + ); + + static::assertSame('https://example.com/path.html', $subject->getUrl()); + } + + /** + * @test + */ + public function returnsUserAgent(): void + { + $language = $this->prophesize(SiteLanguage::class); + + $subject = new Recordview( + 0, + $language->reveal(), + new \DateTimeImmutable(), + '', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0', + 10, + 'sys_category' + ); + + static::assertSame( + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0', + $subject->getUserAgent() + ); + } + + /** + * @test + */ + public function returnsRecordUid(): void + { + $language = $this->prophesize(SiteLanguage::class); + + $subject = new Recordview( + 0, + $language->reveal(), + new \DateTimeImmutable(), + '', + '', + 10, + 'sys_category' + ); + + static::assertSame( + 10, + $subject->getRecordUid() + ); + } + + /** + * @test + */ + public function returnsTableName(): void + { + $language = $this->prophesize(SiteLanguage::class); + + $subject = new Recordview( + 0, + $language->reveal(), + new \DateTimeImmutable(), + '', + '', + 10, + 'sys_category' + ); + + static::assertSame( + 'sys_category', + $subject->getTableName() + ); + } + + /** + * @test + */ + public function returnsOperatingSystem(): void + { + $language = $this->prophesize(SiteLanguage::class); + + $subject = new Recordview( + 0, + $language->reveal(), + new \DateTimeImmutable(), + '', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.116 Safari/537.36', + 10, + 'sys_category' + ); + + static::assertSame( + 'Linux', + $subject->getOperatingSystem() + ); + } +} diff --git a/Tests/Unit/Domain/Recordview/FactoryTest.php b/Tests/Unit/Domain/Recordview/FactoryTest.php new file mode 100644 index 0000000..81da698 --- /dev/null +++ b/Tests/Unit/Domain/Recordview/FactoryTest.php @@ -0,0 +1,255 @@ + + * + * 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\RecordRule; +use DanielSiepmann\Tracking\Domain\Model\Recordview; +use DanielSiepmann\Tracking\Domain\Recordview\Factory; +use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Routing\PageArguments; +use TYPO3\CMS\Core\Site\Entity\SiteLanguage; + +/** + * @covers DanielSiepmann\Tracking\Domain\Recordview\Factory + */ +class FactoryTest extends TestCase +{ + use ProphecyTrait; + + /** + * @test + */ + public function returnsRecordviewFromRequest(): void + { + $rule = $this->prophesize(RecordRule::class); + $rule->getUidExpression()->willReturn('request.getQueryParams()["category"] > 0'); + $rule->getTableName()->willReturn('sys_category'); + + $routing = $this->prophesize(PageArguments::class); + $routing->getPageId()->willReturn(10); + + $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([]); + $request->getQueryParams()->willReturn([ + 'category' => 10, + ]); + + $result = Factory::fromRequest($request->reveal(), $rule->reveal()); + static::assertInstanceOf(Recordview::class, $result); + } + + /** + * @test + */ + public function returnedRecordviewContainsUserAgent(): void + { + $rule = $this->prophesize(RecordRule::class); + $rule->getUidExpression()->willReturn('request.getQueryParams()["category"] > 0'); + $rule->getTableName()->willReturn('sys_category'); + + $routing = $this->prophesize(PageArguments::class); + $routing->getPageId()->willReturn(10); + + $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(['Some User Agent']); + $request->getQueryParams()->willReturn([ + 'category' => 10, + ]); + + $result = Factory::fromRequest($request->reveal(), $rule->reveal()); + static::assertSame('Some User Agent', $result->getUserAgent()); + } + + /** + * @test + */ + public function returnedRecordviewContainsUri(): void + { + $rule = $this->prophesize(RecordRule::class); + $rule->getUidExpression()->willReturn('request.getQueryParams()["category"] > 0'); + $rule->getTableName()->willReturn('sys_category'); + + $routing = $this->prophesize(PageArguments::class); + $routing->getPageId()->willReturn(10); + + $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'); + $request->getHeader('User-Agent')->willReturn(['']); + $request->getQueryParams()->willReturn([ + 'category' => 10, + ]); + + $result = Factory::fromRequest($request->reveal(), $rule->reveal()); + static::assertSame('https://example.com', $result->getUrl()); + } + + /** + * @test + */ + public function returnedRecordviewContainsDateTime(): void + { + $rule = $this->prophesize(RecordRule::class); + $rule->getUidExpression()->willReturn('request.getQueryParams()["category"] > 0'); + $rule->getTableName()->willReturn('sys_category'); + + $routing = $this->prophesize(PageArguments::class); + $routing->getPageId()->willReturn(10); + + $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'); + $request->getHeader('User-Agent')->willReturn(['']); + $request->getQueryParams()->willReturn([ + 'category' => 10, + ]); + + $result = Factory::fromRequest($request->reveal(), $rule->reveal()); + static::assertInstanceOf(\DateTimeImmutable::class, $result->getCrdate()); + } + + /** + * @test + */ + public function returnedRecordviewContainsLanguage(): void + { + $rule = $this->prophesize(RecordRule::class); + $rule->getUidExpression()->willReturn('request.getQueryParams()["category"] > 0'); + $rule->getTableName()->willReturn('sys_category'); + + $routing = $this->prophesize(PageArguments::class); + $routing->getPageId()->willReturn(10); + + $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'); + $request->getHeader('User-Agent')->willReturn(['']); + $request->getQueryParams()->willReturn([ + 'category' => 10, + ]); + + $result = Factory::fromRequest($request->reveal(), $rule->reveal()); + static::assertSame($language->reveal(), $result->getLanguage()); + } + + /** + * @test + */ + public function returnedRecordviewContainsPageId(): void + { + $rule = $this->prophesize(RecordRule::class); + $rule->getUidExpression()->willReturn('request.getQueryParams()["category"] > 0'); + $rule->getTableName()->willReturn('sys_category'); + + $routing = $this->prophesize(PageArguments::class); + $routing->getPageId()->willReturn(10); + + $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'); + $request->getHeader('User-Agent')->willReturn(['']); + $request->getQueryParams()->willReturn([ + 'category' => 10, + ]); + + $result = Factory::fromRequest($request->reveal(), $rule->reveal()); + static::assertSame(10, $result->getPageUid()); + } + + /** + * @test + */ + public function returnedRecordviewContainsRecordUid(): void + { + $rule = $this->prophesize(RecordRule::class); + $rule->getUidExpression()->willReturn('request.getQueryParams()["category"]'); + $rule->getTableName()->willReturn('sys_category'); + + $routing = $this->prophesize(PageArguments::class); + $routing->getPageId()->willReturn(10); + + $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'); + $request->getHeader('User-Agent')->willReturn(['']); + $request->getQueryParams()->willReturn([ + 'category' => 20, + ]); + + $result = Factory::fromRequest($request->reveal(), $rule->reveal()); + static::assertSame(20, $result->getRecordUid()); + } + + /** + * @test + */ + public function returnedRecordviewContainsTableName(): void + { + $rule = $this->prophesize(RecordRule::class); + $rule->getUidExpression()->willReturn('request.getQueryParams()["category"] > 0'); + $rule->getTableName()->willReturn('sys_category'); + + $routing = $this->prophesize(PageArguments::class); + $routing->getPageId()->willReturn(10); + + $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'); + $request->getHeader('User-Agent')->willReturn(['']); + $request->getQueryParams()->willReturn([ + 'category' => 20, + ]); + + $result = Factory::fromRequest($request->reveal(), $rule->reveal()); + static::assertSame('sys_category', $result->getTableName()); + } +} diff --git a/Tests/Unit/Domain/Repository/RecordviewTest.php b/Tests/Unit/Domain/Repository/RecordviewTest.php new file mode 100644 index 0000000..1aced83 --- /dev/null +++ b/Tests/Unit/Domain/Repository/RecordviewTest.php @@ -0,0 +1,80 @@ + + * + * 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\Recordview as Model; +use DanielSiepmann\Tracking\Domain\Repository\Recordview; +use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; +use TYPO3\CMS\Core\Database\Connection; +use TYPO3\CMS\Core\Site\Entity\SiteLanguage; + +/** + * @covers DanielSiepmann\Tracking\Domain\Repository\Recordview + */ +class RecordviewTest extends TestCase +{ + use ProphecyTrait; + + /** + * @test + */ + public function modelCanBeAdded(): void + { + $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->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'); + $model->getOperatingSystem()->willReturn('Linux'); + $model->getRecordUid()->willReturn(10); + $model->getTableName()->willReturn('sys_category'); + + $connection->insert( + 'tx_tracking_recordview', + [ + 'pid' => 10, + 'crdate' => 1582660189, + 'tstamp' => 1582660189, + 'sys_language_uid' => 2, + 'url' => 'https://example.com/path.html', + 'user_agent' => 'Mozilla/5.0 (Windows NT 10.0) Gecko/20100101 Firefox/74.0', + 'operating_system' => 'Linux', + 'record_uid' => 10, + 'record_table_name' => 'sys_category', + 'record' => 'sys_category_10', + ] + )->willReturn(1)->shouldBeCalledTimes(1); + + $subject = new Recordview($connection->reveal()); + $subject->add($model->reveal()); + } +} diff --git a/Tests/Unit/Middleware/RecordviewTest.php b/Tests/Unit/Middleware/RecordviewTest.php new file mode 100644 index 0000000..e14994c --- /dev/null +++ b/Tests/Unit/Middleware/RecordviewTest.php @@ -0,0 +1,188 @@ + + * + * 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\Recordview as Model; +use DanielSiepmann\Tracking\Domain\Repository\Recordview as Repository; +use DanielSiepmann\Tracking\Middleware\Recordview; +use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; +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\Recordview + */ +class RecordviewTest extends TestCase +{ + use ProphecyTrait; + + /** + * @test + */ + public function proceedsWithNextHandlerIfNoRuleIsConfigured(): void + { + $repository = $this->prophesize(Repository::class); + $repository->add()->shouldNotBeCalled(); + $context = $this->prophesize(Context::class); + + $request = $this->prophesize(ServerRequestInterface::class); + $response = $this->prophesize(ResponseInterface::class); + $handler = $this->prophesize(RequestHandlerInterface::class); + $handler->handle($request->reveal())->willReturn($response); + + $subject = new Recordview($repository->reveal(), $context->reveal(), []); + $result = $subject->process($request->reveal(), $handler->reveal()); + + static::assertInstanceOf(ResponseInterface::class, $result); + } + + /** + * @test + */ + public function doesntAddViewIfRuleDoesNotMatchRequest(): void + { + $repository = $this->prophesize(Repository::class); + $repository->add()->shouldNotBeCalled(); + $context = $this->prophesize(Context::class); + + $request = $this->prophesize(ServerRequestInterface::class); + $request->getQueryParams()->willReturn([]); + $response = $this->prophesize(ResponseInterface::class); + $handler = $this->prophesize(RequestHandlerInterface::class); + $handler->handle($request->reveal())->willReturn($response); + + $subject = new Recordview($repository->reveal(), $context->reveal(), [ + 'topic' => [ + 'matches' => 'request.getQueryParams()["topic_id"] > 0', + 'recordUid' => '', + 'tableName' => '', + ], + ]); + $result = $subject->process($request->reveal(), $handler->reveal()); + + static::assertInstanceOf(ResponseInterface::class, $result); + } + + /** + * @test + */ + public function addsSingleViewIfRuleMatches(): void + { + $repository = $this->prophesize(Repository::class); + $repository->add(Argument::that(function (Model $recordview) { + return $recordview->getPageUid() === 10 + && $recordview->getRecordUid() === 10 + && $recordview->getTableName() === 'topics' + ; + }))->shouldBeCalled(); + $context = $this->prophesize(Context::class); + + $routing = $this->prophesize(PageArguments::class); + $routing->getPageId()->willReturn(10); + + $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([]); + $request->getQueryParams()->willReturn([ + 'topic_id' => '10', + ]); + $response = $this->prophesize(ResponseInterface::class); + $handler = $this->prophesize(RequestHandlerInterface::class); + $handler->handle($request->reveal())->willReturn($response); + + $subject = new Recordview($repository->reveal(), $context->reveal(), [ + 'topic' => [ + 'matches' => 'request.getQueryParams()["topic_id"] > 0', + 'recordUid' => 'request.getQueryParams()["topic_id"]', + 'tableName' => 'topics', + ], + ]); + $result = $subject->process($request->reveal(), $handler->reveal()); + + static::assertInstanceOf(ResponseInterface::class, $result); + } + + /** + * @test + */ + public function canAddMultipleViewsIfMultipleRulesApply(): void + { + $repository = $this->prophesize(Repository::class); + $repository->add(Argument::that(function (Model $recordview) { + return $recordview->getPageUid() === 10 + && $recordview->getRecordUid() === 10 + && $recordview->getTableName() === 'topics' + ; + }))->shouldBeCalled(); + $repository->add(Argument::that(function (Model $recordview) { + return $recordview->getPageUid() === 10 + && $recordview->getRecordUid() === 20 + && $recordview->getTableName() === 'news' + ; + }))->shouldBeCalled(); + $context = $this->prophesize(Context::class); + + $routing = $this->prophesize(PageArguments::class); + $routing->getPageId()->willReturn(10); + + $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([]); + $request->getQueryParams()->willReturn([ + 'topic_id' => '10', + 'news' => '20', + ]); + $response = $this->prophesize(ResponseInterface::class); + $handler = $this->prophesize(RequestHandlerInterface::class); + $handler->handle($request->reveal())->willReturn($response); + + $subject = new Recordview($repository->reveal(), $context->reveal(), [ + 'topic' => [ + 'matches' => 'request.getQueryParams()["topic_id"] > 0', + 'recordUid' => 'request.getQueryParams()["topic_id"]', + 'tableName' => 'topics', + ], + 'news' => [ + 'matches' => 'request.getQueryParams()["news"] > 0', + 'recordUid' => 'request.getQueryParams()["news"]', + 'tableName' => 'news', + ], + ]); + $result = $subject->process($request->reveal(), $handler->reveal()); + + static::assertInstanceOf(ResponseInterface::class, $result); + } +} diff --git a/ext_tables.sql b/ext_tables.sql index 471859d..85ed111 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -4,3 +4,12 @@ CREATE TABLE tx_tracking_pageview ( operating_system varchar(255) DEFAULT '' NOT NULL, type int(11) unsigned DEFAULT '0' NOT NULL, ); + +CREATE TABLE tx_tracking_recordview ( + url text, + user_agent text, + operating_system varchar(255) DEFAULT '' NOT NULL, + record varchar(255) DEFAULT '' NOT NULL, + record_uid int(11) unsigned DEFAULT '0' NOT NULL, + record_table_name varchar(255) DEFAULT '' NOT NULL, +); diff --git a/phpstan.neon b/phpstan.neon index 51c13e7..a81c62b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,5 +6,6 @@ parameters: checkGenericClassInNonGenericObjectType: false checkMissingIterableValueType: false ignoreErrors: + - '#Cannot call method fetch\(\) on Doctrine\\DBAL\\Driver\\Statement\|int\.#' - '#Cannot call method fetchAll\(\) on Doctrine\\DBAL\\Driver\\Statement\|int\.#' - '#Cannot call method fetchColumn\(\) on Doctrine\\DBAL\\Driver\\Statement\|int\.#'