Add tracking of record views

Allow Integrator to define rules to track views of records.
This allows to add tracking to extensions like tx_news.
Integrators are able to define matching rules on current request. If a
request matches, the record is stored as individual view beside existing
pageview.

Relates: #14
This commit is contained in:
Daniel Siepmann 2020-07-29 10:07:14 +02:00
parent 44ca6a2d3c
commit 33685d1735
25 changed files with 1843 additions and 95 deletions

View file

@ -0,0 +1,207 @@
<?php
namespace DanielSiepmann\Tracking\Dashboard\Provider;
/*
* Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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<int>
*/
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] ?? '',
];
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace DanielSiepmann\Tracking\Domain\Model;
/*
* Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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 '';
}
}

View file

@ -1,6 +1,6 @@
<?php <?php
namespace DanielSiepmann\Tracking\Domain\Pageview; namespace DanielSiepmann\Tracking\Domain\Model;
/* /*
* Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de> * Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de>
@ -21,10 +21,7 @@ namespace DanielSiepmann\Tracking\Domain\Pageview;
* 02110-1301, USA. * 02110-1301, USA.
*/ */
use DanielSiepmann\Tracking\Domain\Model\Pageview; interface HasUserAgent
use Psr\Http\Message\ServerRequestInterface;
interface FromRequest
{ {
public static function fromRequest(ServerRequestInterface $request): Pageview; public function getUserAgent(): string;
} }

View file

@ -23,7 +23,7 @@ namespace DanielSiepmann\Tracking\Domain\Model;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage; use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
class Pageview class Pageview implements HasUserAgent
{ {
/** /**
* @var int * @var int
@ -115,31 +115,6 @@ class Pageview
public function getOperatingSystem(): string public function getOperatingSystem(): string
{ {
if (mb_stripos($this->userAgent, 'Android') !== false) { return Extractor::getOperatingSystem($this);
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 '';
} }
} }

View file

@ -0,0 +1,96 @@
<?php
namespace DanielSiepmann\Tracking\Domain\Model;
/*
* Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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;
}
}

View file

@ -0,0 +1,120 @@
<?php
namespace DanielSiepmann\Tracking\Domain\Model;
/*
* Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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);
}
}

View file

@ -26,7 +26,7 @@ use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Routing\PageArguments; use TYPO3\CMS\Core\Routing\PageArguments;
use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Core\Site\SiteFinder;
class Factory implements FromRequest class Factory
{ {
/** /**
* @var SiteFinder * @var SiteFinder
@ -40,7 +40,7 @@ class Factory implements FromRequest
public static function fromRequest(ServerRequestInterface $request): Pageview public static function fromRequest(ServerRequestInterface $request): Pageview
{ {
return new PageView( return new Pageview(
static::getRouting($request)->getPageId(), static::getRouting($request)->getPageId(),
$request->getAttribute('language'), $request->getAttribute('language'),
new \DateTimeImmutable(), new \DateTimeImmutable(),
@ -52,7 +52,7 @@ class Factory implements FromRequest
public function fromDbRow(array $dbRow): Pageview public function fromDbRow(array $dbRow): Pageview
{ {
return new PageView( return new Pageview(
$dbRow['pid'], $dbRow['pid'],
$this->siteFinder->getSiteByPageId($dbRow['pid'])->getLanguageById($dbRow['sys_language_uid']), $this->siteFinder->getSiteByPageId($dbRow['pid'])->getLanguageById($dbRow['sys_language_uid']),
new \DateTimeImmutable('@' . $dbRow['crdate']), new \DateTimeImmutable('@' . $dbRow['crdate']),

View file

@ -0,0 +1,57 @@
<?php
namespace DanielSiepmann\Tracking\Domain\Recordview;
/*
* Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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');
}
}

View file

@ -23,7 +23,6 @@ namespace DanielSiepmann\Tracking\Domain\Repository;
use DanielSiepmann\Tracking\Domain\Model\Pageview as Model; use DanielSiepmann\Tracking\Domain\Model\Pageview as Model;
use DanielSiepmann\Tracking\Domain\Pageview\Factory; use DanielSiepmann\Tracking\Domain\Pageview\Factory;
use Doctrine\DBAL\Driver\Statement;
use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Database\Connection;
class Pageview class Pageview
@ -60,10 +59,8 @@ class Pageview
$queryBuilder = $this->connection->createQueryBuilder(); $queryBuilder = $this->connection->createQueryBuilder();
$pageViews = $queryBuilder->select('*')->from('tx_tracking_pageview')->execute(); $pageViews = $queryBuilder->select('*')->from('tx_tracking_pageview')->execute();
if ($pageViews instanceof Statement) { while ($pageView = $pageViews->fetch()) {
while ($pageView = $pageViews->fetch()) { yield $this->factory->fromDbRow($pageView);
yield $this->factory->fromDbRow($pageView);
}
} }
} }

View file

@ -0,0 +1,63 @@
<?php
namespace DanielSiepmann\Tracking\Domain\Repository;
/*
* Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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(),
];
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace DanielSiepmann\Tracking\Middleware;
/*
* Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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<RecordRule>
*/
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,
]
);
}
}

View file

@ -2,7 +2,7 @@
return [ return [
'frontend' => [ 'frontend' => [
'tracking-pageview' => [ 'danielsiepmann/tracking/pageview' => [
'target' => \DanielSiepmann\Tracking\Middleware\Pageview::class, 'target' => \DanielSiepmann\Tracking\Middleware\Pageview::class,
'before' => [ 'before' => [
'typo3/cms-frontend/content-length-headers', 'typo3/cms-frontend/content-length-headers',
@ -11,5 +11,14 @@ return [
'typo3/cms-frontend/shortcut-and-mountpoint-redirect', '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',
],
],
], ],
]; ];

View file

@ -27,11 +27,32 @@ services:
arguments: arguments:
- 'tx_tracking_pageview' - '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: DanielSiepmann\Tracking\Domain\Repository\Pageview:
public: true public: true
arguments: arguments:
- '@dbconnection.tx_tracking_pageview' - '@dbconnection.tx_tracking_pageview'
DanielSiepmann\Tracking\Domain\Repository\Recordview:
public: true
arguments:
- '@dbconnection.tx_tracking_recordview'
DanielSiepmann\Tracking\Middleware\Pageview: DanielSiepmann\Tracking\Middleware\Pageview:
public: true public: true
arguments: 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 "/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/") and not (request.getHeader("User-Agent")[0] matches "/mattermost|Slackbot|WhatsApp/")
DanielSiepmann\Tracking\Middleware\Recordview:
public: true
arguments:
$rules: []
DanielSiepmann\Tracking\Command\UpdateDataCommand: DanielSiepmann\Tracking\Command\UpdateDataCommand:
tags: tags:
- name: 'console.command' - name: 'console.command'

View file

@ -1,4 +1,5 @@
<?php <?php
return [ return [
'ctrl' => [ 'ctrl' => [
'label' => 'url', 'label' => 'url',
@ -22,7 +23,7 @@ return [
'pid' => [ 'pid' => [
'label' => 'LLL:EXT:tracking/Resources/Private/Language/locallang_tca.xlf:table.pageview.pid', 'label' => 'LLL:EXT:tracking/Resources/Private/Language/locallang_tca.xlf:table.pageview.pid',
'config' => [ '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. // This will break internal PID handling.
'type' => 'input', 'type' => 'input',
'readOnly' => true, 'readOnly' => true,
@ -75,7 +76,6 @@ return [
'readOnly' => true, 'readOnly' => true,
'type' => 'input', 'type' => 'input',
'size' => 50, 'size' => 50,
'max' => 255,
], ],
], ],
], ],

View file

@ -0,0 +1,85 @@
<?php
return [
'ctrl' => [
'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,
],
],
],
];

View file

@ -30,6 +30,34 @@
<trans-unit id="table.pageview.operating_system"> <trans-unit id="table.pageview.operating_system">
<source>Operating System</source> <source>Operating System</source>
</trans-unit> </trans-unit>
<trans-unit id="table.recordview">
<source>Recordview</source>
</trans-unit>
<trans-unit id="table.recordview.pid">
<source>Page</source>
</trans-unit>
<trans-unit id="table.recordview.record">
<source>Record</source>
</trans-unit>
<trans-unit id="table.recordview.url">
<source>URL</source>
</trans-unit>
<trans-unit id="table.recordview.sys_language">
<source>System language</source>
</trans-unit>
<trans-unit id="table.recordview.sys_language.0">
<source>Default system language</source>
</trans-unit>
<trans-unit id="table.recordview.crdate">
<source>Date + Time</source>
</trans-unit>
<trans-unit id="table.recordview.user_agent">
<source>User agent</source>
</trans-unit>
<trans-unit id="table.recordview.operating_system">
<source>Operating System</source>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -0,0 +1,97 @@
<?php
namespace DanielSiepmann\Tracking\Tests\Unit\Domain\Model;
/*
* Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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',
],
];
}
}

View file

@ -217,10 +217,8 @@ class PageviewTest extends TestCase
/** /**
* @test * @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); $language = $this->prophesize(SiteLanguage::class);
@ -230,58 +228,12 @@ class PageviewTest extends TestCase
new \DateTimeImmutable(), new \DateTimeImmutable(),
0, 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( static::assertSame(
$expectedOperatingSystem, 'Linux',
$subject->getOperatingSystem() $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',
],
];
}
} }

View file

@ -0,0 +1,129 @@
<?php
namespace DanielSiepmann\Tracking\Tests\Unit\Domain\Model;
/*
* Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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());
}
}

View file

@ -0,0 +1,228 @@
<?php
namespace DanielSiepmann\Tracking\Tests\Unit\Domain\Model;
/*
* Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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()
);
}
}

View file

@ -0,0 +1,255 @@
<?php
namespace DanielSiepmann\Tracking\Tests\Unit\Domain\Recordview;
/*
* Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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());
}
}

View file

@ -0,0 +1,80 @@
<?php
namespace DanielSiepmann\Tracking\Tests\Unit\Domain\Repository;
/*
* Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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());
}
}

View file

@ -0,0 +1,188 @@
<?php
namespace DanielSiepmann\Tracking\Tests\Unit\Middleware;
/*
* Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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);
}
}

View file

@ -4,3 +4,12 @@ CREATE TABLE tx_tracking_pageview (
operating_system varchar(255) DEFAULT '' NOT NULL, operating_system varchar(255) DEFAULT '' NOT NULL,
type int(11) unsigned DEFAULT '0' 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,
);

View file

@ -6,5 +6,6 @@ parameters:
checkGenericClassInNonGenericObjectType: false checkGenericClassInNonGenericObjectType: false
checkMissingIterableValueType: false checkMissingIterableValueType: false
ignoreErrors: ignoreErrors:
- '#Cannot call method fetch\(\) on Doctrine\\DBAL\\Driver\\Statement\|int\.#'
- '#Cannot call method fetchAll\(\) 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\.#' - '#Cannot call method fetchColumn\(\) on Doctrine\\DBAL\\Driver\\Statement\|int\.#'