mirror of
https://github.com/werkraum-media/abtest.git
synced 2024-11-21 16:56:11 +01:00
Initial migration
We will use and maintain the extension for one of our customers. We add our basic setup. We add tests. We refactor code. We use newer APIs of TYPO3. We will integrate Matomo A/B Testing afterwards as next step.
This commit is contained in:
parent
8fdd877d4a
commit
604110e737
50 changed files with 1396 additions and 4563 deletions
13
.gitattributes
vendored
Normal file
13
.gitattributes
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
Tests export-ignore
|
||||||
|
patches export-ignore
|
||||||
|
.github export-ignore
|
||||||
|
|
||||||
|
.gitattributes export-ignore
|
||||||
|
.gitignore export-ignore
|
||||||
|
|
||||||
|
rector export-ignore
|
||||||
|
.php-cs-fixer.dist.php export-ignore
|
||||||
|
phpstan.neon export-ignore
|
||||||
|
phpunit.xml.dist export-ignore
|
||||||
|
|
||||||
|
shell.nix export-ignore
|
133
.github/workflows/ci.yaml
vendored
Normal file
133
.github/workflows/ci.yaml
vendored
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
- pull_request
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-composer:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: cachix/install-nix-action@v17
|
||||||
|
with:
|
||||||
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
|
|
||||||
|
- name: Validate composer.json
|
||||||
|
run: nix-shell --pure --run project-validate-composer
|
||||||
|
|
||||||
|
php-linting:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
php-version:
|
||||||
|
- 7.4
|
||||||
|
- 8.0
|
||||||
|
- 8.1
|
||||||
|
- 8.2
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: "${{ matrix.php-version }}"
|
||||||
|
coverage: none
|
||||||
|
tools: composer:v2
|
||||||
|
|
||||||
|
- name: PHP lint
|
||||||
|
run: "find *.php Classes Configuration Tests -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l"
|
||||||
|
|
||||||
|
xml-linting:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: cachix/install-nix-action@v17
|
||||||
|
with:
|
||||||
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
|
|
||||||
|
- name: Validate XML
|
||||||
|
run: nix-shell --pure --run project-validate-xml
|
||||||
|
|
||||||
|
coding-guideline:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: cachix/install-nix-action@v17
|
||||||
|
with:
|
||||||
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
|
|
||||||
|
- name: Check Coding Guideline
|
||||||
|
run: nix-shell --pure --run project-coding-guideline
|
||||||
|
|
||||||
|
code-quality:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- php-version: '7.4'
|
||||||
|
- php-version: '8.0'
|
||||||
|
- php-version: '8.1'
|
||||||
|
- php-version: '8.2'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: "${{ matrix.php-version }}"
|
||||||
|
coverage: none
|
||||||
|
tools: composer:v2
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-interaction --prefer-dist --no-progress
|
||||||
|
|
||||||
|
- name: Code Quality (by PHPStan)
|
||||||
|
run: ./vendor/bin/phpstan analyse
|
||||||
|
|
||||||
|
tests:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- php-version: '7.4'
|
||||||
|
- php-version: '8.0'
|
||||||
|
- php-version: '8.1'
|
||||||
|
- php-version: '8.2'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: "${{ matrix.php-version }}"
|
||||||
|
coverage: none
|
||||||
|
tools: composer:v2
|
||||||
|
|
||||||
|
- name: Setup MySQL
|
||||||
|
uses: mirromutth/mysql-action@v1.1
|
||||||
|
with:
|
||||||
|
mysql version: '8'
|
||||||
|
mysql database: 'typo3'
|
||||||
|
mysql root password: 'root'
|
||||||
|
|
||||||
|
- name: Wait for MySQL
|
||||||
|
run: |
|
||||||
|
while ! mysqladmin ping --host=127.0.0.1 --password=root --silent; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-interaction --prefer-dist --no-progress
|
||||||
|
|
||||||
|
- name: PHPUnit Tests
|
||||||
|
env:
|
||||||
|
typo3DatabaseDriver: "pdo_mysql"
|
||||||
|
typo3DatabaseName: "typo3"
|
||||||
|
typo3DatabaseHost: "127.0.0.1"
|
||||||
|
typo3DatabaseUsername: "root"
|
||||||
|
typo3DatabasePassword: "root"
|
||||||
|
run: ./vendor/bin/phpunit --testdox
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/.Build/
|
||||||
|
/vendor/
|
||||||
|
/composer.lock
|
62
.php-cs-fixer.dist.php
Normal file
62
.php-cs-fixer.dist.php
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
$finder = (new PhpCsFixer\Finder())
|
||||||
|
->ignoreVCSIgnored(true)
|
||||||
|
->in(realpath(__DIR__));
|
||||||
|
|
||||||
|
return (new \PhpCsFixer\Config())
|
||||||
|
->setRiskyAllowed(true)
|
||||||
|
->setRules([
|
||||||
|
'@DoctrineAnnotation' => true,
|
||||||
|
'@PSR2' => true,
|
||||||
|
'array_syntax' => ['syntax' => 'short'],
|
||||||
|
'blank_line_after_opening_tag' => true,
|
||||||
|
'braces' => ['allow_single_line_closure' => true],
|
||||||
|
'cast_spaces' => ['space' => 'none'],
|
||||||
|
'compact_nullable_typehint' => true,
|
||||||
|
'concat_space' => ['spacing' => 'one'],
|
||||||
|
'declare_equal_normalize' => ['space' => 'none'],
|
||||||
|
'dir_constant' => true,
|
||||||
|
'function_to_constant' => ['functions' => ['get_called_class', 'get_class', 'get_class_this', 'php_sapi_name', 'phpversion', 'pi']],
|
||||||
|
'function_typehint_space' => true,
|
||||||
|
'lowercase_cast' => true,
|
||||||
|
'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
|
||||||
|
'modernize_strpos' => true,
|
||||||
|
'modernize_types_casting' => true,
|
||||||
|
'native_function_casing' => true,
|
||||||
|
'new_with_braces' => true,
|
||||||
|
'no_alias_functions' => true,
|
||||||
|
'no_blank_lines_after_phpdoc' => true,
|
||||||
|
'no_empty_phpdoc' => true,
|
||||||
|
'no_empty_statement' => true,
|
||||||
|
'no_extra_blank_lines' => true,
|
||||||
|
'no_leading_import_slash' => true,
|
||||||
|
'no_leading_namespace_whitespace' => true,
|
||||||
|
'no_null_property_initialization' => true,
|
||||||
|
'no_short_bool_cast' => true,
|
||||||
|
'no_singleline_whitespace_before_semicolons' => true,
|
||||||
|
'no_superfluous_elseif' => true,
|
||||||
|
'no_trailing_comma_in_singleline_array' => true,
|
||||||
|
'no_unneeded_control_parentheses' => true,
|
||||||
|
'no_unused_imports' => true,
|
||||||
|
'no_useless_else' => true,
|
||||||
|
'no_whitespace_in_blank_line' => true,
|
||||||
|
'ordered_imports' => true,
|
||||||
|
'php_unit_construct' => ['assertions' => ['assertEquals', 'assertSame', 'assertNotEquals', 'assertNotSame']],
|
||||||
|
'php_unit_mock_short_will_return' => true,
|
||||||
|
'php_unit_test_case_static_method_calls' => ['call_type' => 'self'],
|
||||||
|
'phpdoc_no_access' => true,
|
||||||
|
'phpdoc_no_empty_return' => true,
|
||||||
|
'phpdoc_no_package' => true,
|
||||||
|
'phpdoc_scalar' => true,
|
||||||
|
'phpdoc_trim' => true,
|
||||||
|
'phpdoc_types' => true,
|
||||||
|
'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'],
|
||||||
|
'return_type_declaration' => ['space_before' => 'none'],
|
||||||
|
'single_quote' => true,
|
||||||
|
'single_line_comment_style' => ['comment_types' => ['hash']],
|
||||||
|
'single_trait_insert_per_statement' => true,
|
||||||
|
'trailing_comma_in_multiline' => ['elements' => ['arrays']],
|
||||||
|
'whitespace_after_comma_in_array' => true,
|
||||||
|
'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false],
|
||||||
|
])
|
||||||
|
->setFinder($finder);
|
81
Classes/Cookie.php
Normal file
81
Classes/Cookie.php
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2023 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WerkraumMedia\ABTest;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\SingletonInterface;
|
||||||
|
|
||||||
|
class Cookie implements SingletonInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $requestedPageUid = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $actualPageUid = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $lifetime = 604800;
|
||||||
|
|
||||||
|
public function setRequestedPage(int $uid): void
|
||||||
|
{
|
||||||
|
$this->requestedPageUid = $uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setActualPage(int $uid): void
|
||||||
|
{
|
||||||
|
$this->actualPageUid = $uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLifetime(int $seconds): void
|
||||||
|
{
|
||||||
|
if ($seconds > 0) {
|
||||||
|
$this->lifetime = $seconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRequestedPage(): int
|
||||||
|
{
|
||||||
|
return $this->requestedPageUid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getActualPage(): int
|
||||||
|
{
|
||||||
|
return $this->actualPageUid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLifetime(): int
|
||||||
|
{
|
||||||
|
return $this->lifetime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function needsToBeSet(): bool
|
||||||
|
{
|
||||||
|
return $this->actualPageUid > 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,263 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Device Detector - The Universal Device Detection library for parsing User Agents
|
|
||||||
*
|
|
||||||
* @link http://piwik.org
|
|
||||||
* @license http://www.gnu.org/licenses/lgpl.html LGPL v3 or later
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace WapplerSystems\ABTest2\DeviceDetector;
|
|
||||||
|
|
||||||
use TYPO3\CMS\Core\Utility\DebugUtility;
|
|
||||||
use WapplerSystems\ABTest2\DeviceDetector\Parser\Bot;
|
|
||||||
use WapplerSystems\ABTest2\DeviceDetector\Yaml\Parser AS YamlParser;
|
|
||||||
use WapplerSystems\ABTest2\DeviceDetector\Yaml\Spyc;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class DeviceDetector
|
|
||||||
|
|
||||||
*
|
|
||||||
* @package DeviceDetector
|
|
||||||
*/
|
|
||||||
class DeviceDetector
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Constant used as value for unknown browser / os
|
|
||||||
*/
|
|
||||||
const UNKNOWN = "UNK";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds the useragent that should be parsed
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $userAgent;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds bot information if parsing the UA results in a bot
|
|
||||||
* (All other information attributes will stay empty in that case)
|
|
||||||
*
|
|
||||||
* If $discardBotInformation is set to true, this property will be set to
|
|
||||||
* true if parsed UA is identified as bot, additional information will be not available
|
|
||||||
*
|
|
||||||
* If $skipBotDetection is set to true, bot detection will not be performed and isBot will
|
|
||||||
* always be false
|
|
||||||
*
|
|
||||||
* @var array|boolean
|
|
||||||
*/
|
|
||||||
protected $bot = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $discardBotInformation = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $skipBotDetection = false;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds the parser class used for parsing yml-Files
|
|
||||||
* @var \WapplerSystems\ABTest2\DeviceDetector\Yaml\Parser
|
|
||||||
*/
|
|
||||||
protected $yamlParser = null;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
private $parsed = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*
|
|
||||||
* @param string $userAgent UA to parse
|
|
||||||
*/
|
|
||||||
public function __construct($userAgent = '')
|
|
||||||
{
|
|
||||||
if ($userAgent != '') {
|
|
||||||
$this->setUserAgent($userAgent);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the useragent to be parsed
|
|
||||||
*
|
|
||||||
* @param string $userAgent
|
|
||||||
*/
|
|
||||||
public function setUserAgent($userAgent)
|
|
||||||
{
|
|
||||||
if ($this->userAgent != $userAgent) {
|
|
||||||
$this->reset();
|
|
||||||
}
|
|
||||||
$this->userAgent = $userAgent;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function reset()
|
|
||||||
{
|
|
||||||
$this->bot = null;
|
|
||||||
$this->parsed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether to discard additional bot information
|
|
||||||
* If information is discarded it's only possible check whether UA was detected as bot or not.
|
|
||||||
* (Discarding information speeds up the detection a bit)
|
|
||||||
*
|
|
||||||
* @param bool $discard
|
|
||||||
*/
|
|
||||||
public function discardBotInformation($discard = true)
|
|
||||||
{
|
|
||||||
$this->discardBotInformation = $discard;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether to skip bot detection.
|
|
||||||
* It is needed if we want bots to be processed as a simple clients. So we can detect if it is mobile client,
|
|
||||||
* or desktop, or enything else. By default all this information is not retrieved for the bots.
|
|
||||||
*
|
|
||||||
* @param bool $skip
|
|
||||||
*/
|
|
||||||
public function skipBotDetection($skip = true)
|
|
||||||
{
|
|
||||||
$this->skipBotDetection = $skip;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns if the parsed UA was identified as a Bot
|
|
||||||
*
|
|
||||||
* @see bots.yml for a list of detected bots
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isBot()
|
|
||||||
{
|
|
||||||
return !empty($this->bot);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the user agent that is set to be parsed
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getUserAgent()
|
|
||||||
{
|
|
||||||
return $this->userAgent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the bot extracted from the parsed UA
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function getBot()
|
|
||||||
{
|
|
||||||
return $this->bot;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true, if userAgent was already parsed with parse()
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isParsed()
|
|
||||||
{
|
|
||||||
return $this->parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Triggers the parsing of the current user agent
|
|
||||||
* @throws \Exception
|
|
||||||
*/
|
|
||||||
public function parse()
|
|
||||||
{
|
|
||||||
if ($this->isParsed()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->parsed = true;
|
|
||||||
|
|
||||||
// skip parsing for empty useragents or those not containing any letter
|
|
||||||
if (empty($this->userAgent) || !preg_match('/([a-z])/i', $this->userAgent)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->parseBot();
|
|
||||||
if ($this->isBot()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the UA for bot information using the Bot parser
|
|
||||||
* @throws \Exception
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function parseBot()
|
|
||||||
{
|
|
||||||
if ($this->skipBotDetection) {
|
|
||||||
$this->bot = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$botParser = new Bot();
|
|
||||||
$botParser->setUserAgent($this->getUserAgent());
|
|
||||||
$botParser->setYamlParser($this->getYamlParser());
|
|
||||||
if ($this->discardBotInformation) {
|
|
||||||
$botParser->discardDetails();
|
|
||||||
}
|
|
||||||
$this->bot = $botParser->parse();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
protected function matchUserAgent($regex)
|
|
||||||
{
|
|
||||||
$regex = '/(?:^|[^A-Z_-])(?:' . str_replace('/', '\/', $regex) . ')/i';
|
|
||||||
|
|
||||||
if (preg_match($regex, $this->userAgent, $matches)) {
|
|
||||||
return $matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the Yaml Parser class
|
|
||||||
*
|
|
||||||
* @param YamlParser
|
|
||||||
* @throws \Exception
|
|
||||||
*/
|
|
||||||
public function setYamlParser($yamlParser)
|
|
||||||
{
|
|
||||||
if ($yamlParser instanceof YamlParser) {
|
|
||||||
$this->yamlParser = $yamlParser;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new \Exception('Yaml Parser not supported');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns Yaml Parser object
|
|
||||||
*
|
|
||||||
* @return YamlParser
|
|
||||||
*/
|
|
||||||
public function getYamlParser()
|
|
||||||
{
|
|
||||||
if (!empty($this->yamlParser)) {
|
|
||||||
return $this->yamlParser;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Spyc();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Device Detector - The Universal Device Detection library for parsing User Agents
|
|
||||||
*
|
|
||||||
* @link http://piwik.org
|
|
||||||
* @license http://www.gnu.org/licenses/lgpl.html LGPL v3 or later
|
|
||||||
*/
|
|
||||||
namespace WapplerSystems\ABTest2\DeviceDetector\Parser;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class Bot
|
|
||||||
*
|
|
||||||
* Parses a user agent for bot information
|
|
||||||
*
|
|
||||||
* Detected bots are defined in regexes/bots.yml
|
|
||||||
*
|
|
||||||
* @package DeviceDetector\Parser
|
|
||||||
*/
|
|
||||||
class Bot extends ParserAbstract
|
|
||||||
{
|
|
||||||
protected $fixtureFile = 'bots.yml';
|
|
||||||
protected $parserName = 'bot';
|
|
||||||
protected $discardDetails = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables information discarding
|
|
||||||
*/
|
|
||||||
public function discardDetails()
|
|
||||||
{
|
|
||||||
$this->discardDetails = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the current UA and checks whether it contains bot information
|
|
||||||
*
|
|
||||||
* @see bots.yml for list of detected bots
|
|
||||||
*
|
|
||||||
* Step 1: Build a big regex containing all regexes and match UA against it
|
|
||||||
* -> If no matches found: return
|
|
||||||
* -> Otherwise:
|
|
||||||
* Step 2: Walk through the list of regexes in bots.yml and try to match every one
|
|
||||||
* -> Return the matched data
|
|
||||||
*
|
|
||||||
* If $discardDetails is set to TRUE, the Step 2 will be skipped
|
|
||||||
* $bot will be set to TRUE instead
|
|
||||||
*
|
|
||||||
* NOTE: Doing the big match before matching every single regex speeds up the detection
|
|
||||||
*/
|
|
||||||
public function parse()
|
|
||||||
{
|
|
||||||
$result = null;
|
|
||||||
|
|
||||||
if ($this->preMatchOverall()) {
|
|
||||||
foreach ($this->getRegexes() as $regex) {
|
|
||||||
$matches = $this->matchUserAgent($regex['regex']);
|
|
||||||
|
|
||||||
if ($matches) {
|
|
||||||
if ($this->discardDetails) {
|
|
||||||
$result = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
unset($regex['regex']);
|
|
||||||
$result = $regex;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,269 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Device Detector - The Universal Device Detection library for parsing User Agents
|
|
||||||
*
|
|
||||||
* @link http://piwik.org
|
|
||||||
* @license http://www.gnu.org/licenses/lgpl.html LGPL v3 or later
|
|
||||||
*/
|
|
||||||
namespace WapplerSystems\ABTest2\DeviceDetector\Parser;
|
|
||||||
|
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
||||||
use WapplerSystems\ABTest2\DeviceDetector\Yaml\Parser;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class ParserAbstract
|
|
||||||
*
|
|
||||||
* @package DeviceDetector\Parser
|
|
||||||
*/
|
|
||||||
abstract class ParserAbstract
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Holds the path to the yml file containing regexes
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $fixtureFile;
|
|
||||||
/**
|
|
||||||
* Holds the internal name of the parser
|
|
||||||
* Used for caching
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $parserName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds the user agent the should be parsed
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $userAgent;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds an array with method that should be available global
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $globalMethods;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds an array with regexes to parse, if already loaded
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $regexList;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates how deep versioning will be detected
|
|
||||||
* if $maxMinorParts is 0 only the major version will be returned
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
protected static $maxMinorParts = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Versioning constant used to set max versioning to major version only
|
|
||||||
* Version examples are: 3, 5, 6, 200, 123, ...
|
|
||||||
*/
|
|
||||||
|
|
||||||
const VERSION_TRUNCATION_MAJOR = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Versioning constant used to set max versioning to minor version
|
|
||||||
* Version examples are: 3.4, 5.6, 6.234, 0.200, 1.23, ...
|
|
||||||
*/
|
|
||||||
const VERSION_TRUNCATION_MINOR = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Versioning constant used to set max versioning to path level
|
|
||||||
* Version examples are: 3.4.0, 5.6.344, 6.234.2, 0.200.3, 1.2.3, ...
|
|
||||||
*/
|
|
||||||
const VERSION_TRUNCATION_PATCH = 2;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Versioning constant used to set versioning to build number
|
|
||||||
* Version examples are: 3.4.0.12, 5.6.334.0, 6.234.2.3, 0.200.3.1, 1.2.3.0, ...
|
|
||||||
*/
|
|
||||||
const VERSION_TRUNCATION_BUILD = 3;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Versioning constant used to set versioning to unlimited (no truncation)
|
|
||||||
*/
|
|
||||||
const VERSION_TRUNCATION_NONE = null;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var Parser
|
|
||||||
*/
|
|
||||||
protected $yamlParser;
|
|
||||||
|
|
||||||
abstract public function parse();
|
|
||||||
|
|
||||||
public function __construct($ua='')
|
|
||||||
{
|
|
||||||
$this->setUserAgent($ua);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set how DeviceDetector should return versions
|
|
||||||
* @param int|null $type Any of the VERSION_TRUNCATION_* constants
|
|
||||||
*/
|
|
||||||
public static function setVersionTruncation($type)
|
|
||||||
{
|
|
||||||
if (in_array($type, array(self::VERSION_TRUNCATION_BUILD,
|
|
||||||
self::VERSION_TRUNCATION_NONE,
|
|
||||||
self::VERSION_TRUNCATION_MAJOR,
|
|
||||||
self::VERSION_TRUNCATION_MINOR,
|
|
||||||
self::VERSION_TRUNCATION_PATCH))) {
|
|
||||||
self::$maxMinorParts = $type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the user agent to parse
|
|
||||||
*
|
|
||||||
* @param string $ua user agent
|
|
||||||
*/
|
|
||||||
public function setUserAgent($ua)
|
|
||||||
{
|
|
||||||
$this->userAgent = $ua;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the internal name of the parser
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getName()
|
|
||||||
{
|
|
||||||
return $this->parserName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the result of the parsed yml file defined in $fixtureFile
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
protected function getRegexes()
|
|
||||||
{
|
|
||||||
if (empty($this->regexList)) {
|
|
||||||
$this->regexList = $this->getYamlParser()->parse(
|
|
||||||
file_get_contents(GeneralUtility::getFileAbsFileName('EXT:abtest2/Configuration/YAML/'.$this->fixtureFile))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return $this->regexList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
protected function getRegexesDirectory()
|
|
||||||
{
|
|
||||||
return dirname(__DIR__);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Matches the useragent against the given regex
|
|
||||||
*
|
|
||||||
* @param $regex
|
|
||||||
* @return array|bool
|
|
||||||
*/
|
|
||||||
protected function matchUserAgent($regex)
|
|
||||||
{
|
|
||||||
// only match if useragent begins with given regex or there is no letter before it
|
|
||||||
$regex = '/(?:^|[^A-Z0-9\-_]|[^A-Z0-9\-]_|sprd-)(?:' . str_replace('/', '\/', $regex) . ')/i';
|
|
||||||
|
|
||||||
if (preg_match($regex, $this->userAgent, $matches)) {
|
|
||||||
return $matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $item
|
|
||||||
* @param array $matches
|
|
||||||
* @return string type
|
|
||||||
*/
|
|
||||||
protected function buildByMatch($item, $matches)
|
|
||||||
{
|
|
||||||
for ($nb=1;$nb<=3;$nb++) {
|
|
||||||
if (strpos($item, '$' . $nb) === false) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$replace = isset($matches[$nb]) ? $matches[$nb] : '';
|
|
||||||
$item = trim(str_replace('$' . $nb, $replace, $item));
|
|
||||||
}
|
|
||||||
return $item;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the version with the given $versionString and $matches
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* $versionString = 'v$2'
|
|
||||||
* $matches = array('version_1_0_1', '1_0_1')
|
|
||||||
* return value would be v1.0.1
|
|
||||||
*
|
|
||||||
* @param $versionString
|
|
||||||
* @param $matches
|
|
||||||
* @return mixed|string
|
|
||||||
*/
|
|
||||||
protected function buildVersion($versionString, $matches)
|
|
||||||
{
|
|
||||||
$versionString = $this->buildByMatch($versionString, $matches);
|
|
||||||
$versionString = str_replace('_', '.', $versionString);
|
|
||||||
if (null !== self::$maxMinorParts && substr_count($versionString, '.') > self::$maxMinorParts) {
|
|
||||||
$versionParts = explode('.', $versionString);
|
|
||||||
$versionParts = array_slice($versionParts, 0, 1+self::$maxMinorParts);
|
|
||||||
$versionString = implode('.', $versionParts);
|
|
||||||
}
|
|
||||||
return trim($versionString, ' .');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests the useragent against a combination of all regexes
|
|
||||||
*
|
|
||||||
* All regexes returned by getRegexes() will be reversed and concated with '|'
|
|
||||||
* Afterwards the big regex will be tested against the user agent
|
|
||||||
*
|
|
||||||
* Method can be used to speed up detections by making a big check before doing checks for every single regex
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
protected function preMatchOverall()
|
|
||||||
{
|
|
||||||
$regexes = $this->getRegexes();
|
|
||||||
|
|
||||||
static $overAllMatch;
|
|
||||||
|
|
||||||
if (empty($overAllMatch)) {
|
|
||||||
// reverse all regexes, so we have the generic one first, which already matches most patterns
|
|
||||||
$overAllMatch = array_reduce(array_reverse($regexes), function ($val1, $val2) {
|
|
||||||
if (!empty($val1)) {
|
|
||||||
return $val1.'|'.$val2['regex'];
|
|
||||||
} else {
|
|
||||||
return $val2['regex'];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->matchUserAgent($overAllMatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the YamlParser class
|
|
||||||
*
|
|
||||||
* @param Parser
|
|
||||||
*/
|
|
||||||
public function setYamlParser($yamlParser)
|
|
||||||
{
|
|
||||||
$this->yamlParser = $yamlParser;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns Parser object
|
|
||||||
*
|
|
||||||
* @return Parser
|
|
||||||
*/
|
|
||||||
public function getYamlParser()
|
|
||||||
{
|
|
||||||
return $this->yamlParser;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,567 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of the Symfony package.
|
|
||||||
*
|
|
||||||
* (c) Fabien Potencier <fabien@symfony.com>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace WapplerSystems\ABTest2\DeviceDetector\Yaml;
|
|
||||||
|
|
||||||
use Symfony\Component\Yaml\Exception\DumpException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inline implements a YAML parser/dumper for the YAML inline syntax.
|
|
||||||
*
|
|
||||||
* @author Fabien Potencier <fabien@symfony.com>
|
|
||||||
*/
|
|
||||||
class Inline
|
|
||||||
{
|
|
||||||
const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\']*(?:\'\'[^\']*)*)\')';
|
|
||||||
|
|
||||||
private static $exceptionOnInvalidType = false;
|
|
||||||
private static $objectSupport = false;
|
|
||||||
private static $objectForMap = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a YAML string to a PHP array.
|
|
||||||
*
|
|
||||||
* @param string $value A YAML string
|
|
||||||
* @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
|
|
||||||
* @param bool $objectSupport true if object support is enabled, false otherwise
|
|
||||||
* @param bool $objectForMap true if maps should return a stdClass instead of array()
|
|
||||||
* @param array $references Mapping of variable names to values
|
|
||||||
*
|
|
||||||
* @return array A PHP array representing the YAML string
|
|
||||||
*
|
|
||||||
* @throws ParseException
|
|
||||||
*/
|
|
||||||
public static function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false, $references = array())
|
|
||||||
{
|
|
||||||
self::$exceptionOnInvalidType = $exceptionOnInvalidType;
|
|
||||||
self::$objectSupport = $objectSupport;
|
|
||||||
self::$objectForMap = $objectForMap;
|
|
||||||
|
|
||||||
$value = trim($value);
|
|
||||||
|
|
||||||
if ('' === $value) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) {
|
|
||||||
$mbEncoding = mb_internal_encoding();
|
|
||||||
mb_internal_encoding('ASCII');
|
|
||||||
}
|
|
||||||
|
|
||||||
$i = 0;
|
|
||||||
switch ($value[0]) {
|
|
||||||
case '[':
|
|
||||||
$result = self::parseSequence($value, $i, $references);
|
|
||||||
++$i;
|
|
||||||
break;
|
|
||||||
case '{':
|
|
||||||
$result = self::parseMapping($value, $i, $references);
|
|
||||||
++$i;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$result = self::parseScalar($value, null, array('"', "'"), $i, true, $references);
|
|
||||||
}
|
|
||||||
|
|
||||||
// some comments are allowed at the end
|
|
||||||
if (preg_replace('/\s+#.*$/A', '', substr($value, $i))) {
|
|
||||||
throw new ParseException(sprintf('Unexpected characters near "%s".', substr($value, $i)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($mbEncoding)) {
|
|
||||||
mb_internal_encoding($mbEncoding);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dumps a given PHP variable to a YAML string.
|
|
||||||
*
|
|
||||||
* @param mixed $value The PHP variable to convert
|
|
||||||
* @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
|
|
||||||
* @param bool $objectSupport true if object support is enabled, false otherwise
|
|
||||||
*
|
|
||||||
* @return string The YAML string representing the PHP array
|
|
||||||
*
|
|
||||||
* @throws DumpException When trying to dump PHP resource
|
|
||||||
*/
|
|
||||||
public static function dump($value, $exceptionOnInvalidType = false, $objectSupport = false)
|
|
||||||
{
|
|
||||||
switch (true) {
|
|
||||||
case is_resource($value):
|
|
||||||
if ($exceptionOnInvalidType) {
|
|
||||||
throw new DumpException(sprintf('Unable to dump PHP resources in a YAML file ("%s").', get_resource_type($value)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'null';
|
|
||||||
case is_object($value):
|
|
||||||
if ($objectSupport) {
|
|
||||||
return '!php/object:'.serialize($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($exceptionOnInvalidType) {
|
|
||||||
throw new DumpException('Object support when dumping a YAML file has been disabled.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'null';
|
|
||||||
case is_array($value):
|
|
||||||
return self::dumpArray($value, $exceptionOnInvalidType, $objectSupport);
|
|
||||||
case null === $value:
|
|
||||||
return 'null';
|
|
||||||
case true === $value:
|
|
||||||
return 'true';
|
|
||||||
case false === $value:
|
|
||||||
return 'false';
|
|
||||||
case ctype_digit($value):
|
|
||||||
return is_string($value) ? "'$value'" : (int) $value;
|
|
||||||
case is_numeric($value):
|
|
||||||
$locale = setlocale(LC_NUMERIC, 0);
|
|
||||||
if (false !== $locale) {
|
|
||||||
setlocale(LC_NUMERIC, 'C');
|
|
||||||
}
|
|
||||||
if (is_float($value)) {
|
|
||||||
$repr = (string) $value;
|
|
||||||
if (is_infinite($value)) {
|
|
||||||
$repr = str_ireplace('INF', '.Inf', $repr);
|
|
||||||
} elseif (floor($value) == $value && $repr == $value) {
|
|
||||||
// Preserve float data type since storing a whole number will result in integer value.
|
|
||||||
$repr = '!!float '.$repr;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$repr = is_string($value) ? "'$value'" : (string) $value;
|
|
||||||
}
|
|
||||||
if (false !== $locale) {
|
|
||||||
setlocale(LC_NUMERIC, $locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $repr;
|
|
||||||
case '' == $value:
|
|
||||||
return "''";
|
|
||||||
case Escaper::requiresDoubleQuoting($value):
|
|
||||||
return Escaper::escapeWithDoubleQuotes($value);
|
|
||||||
case Escaper::requiresSingleQuoting($value):
|
|
||||||
case preg_match(self::getHexRegex(), $value):
|
|
||||||
case preg_match(self::getTimestampRegex(), $value):
|
|
||||||
return Escaper::escapeWithSingleQuotes($value);
|
|
||||||
default:
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dumps a PHP array to a YAML string.
|
|
||||||
*
|
|
||||||
* @param array $value The PHP array to dump
|
|
||||||
* @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
|
|
||||||
* @param bool $objectSupport true if object support is enabled, false otherwise
|
|
||||||
*
|
|
||||||
* @return string The YAML string representing the PHP array
|
|
||||||
*/
|
|
||||||
private static function dumpArray($value, $exceptionOnInvalidType, $objectSupport)
|
|
||||||
{
|
|
||||||
// array
|
|
||||||
$keys = array_keys($value);
|
|
||||||
$keysCount = count($keys);
|
|
||||||
if ((1 === $keysCount && '0' == $keys[0])
|
|
||||||
|| ($keysCount > 1 && array_reduce($keys, function ($v, $w) { return (int) $v + $w; }, 0) === $keysCount * ($keysCount - 1) / 2)
|
|
||||||
) {
|
|
||||||
$output = array();
|
|
||||||
foreach ($value as $val) {
|
|
||||||
$output[] = self::dump($val, $exceptionOnInvalidType, $objectSupport);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sprintf('[%s]', implode(', ', $output));
|
|
||||||
}
|
|
||||||
|
|
||||||
// mapping
|
|
||||||
$output = array();
|
|
||||||
foreach ($value as $key => $val) {
|
|
||||||
$output[] = sprintf('%s: %s', self::dump($key, $exceptionOnInvalidType, $objectSupport), self::dump($val, $exceptionOnInvalidType, $objectSupport));
|
|
||||||
}
|
|
||||||
|
|
||||||
return sprintf('{ %s }', implode(', ', $output));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a scalar to a YAML string.
|
|
||||||
*
|
|
||||||
* @param string $scalar
|
|
||||||
* @param string $delimiters
|
|
||||||
* @param array $stringDelimiters
|
|
||||||
* @param int &$i
|
|
||||||
* @param bool $evaluate
|
|
||||||
* @param array $references
|
|
||||||
*
|
|
||||||
* @return string A YAML string
|
|
||||||
*
|
|
||||||
* @throws ParseException When malformed inline YAML string is parsed
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
public static function parseScalar($scalar, $delimiters = null, $stringDelimiters = array('"', "'"), &$i = 0, $evaluate = true, $references = array())
|
|
||||||
{
|
|
||||||
if (in_array($scalar[$i], $stringDelimiters)) {
|
|
||||||
// quoted scalar
|
|
||||||
$output = self::parseQuotedScalar($scalar, $i);
|
|
||||||
|
|
||||||
if (null !== $delimiters) {
|
|
||||||
$tmp = ltrim(substr($scalar, $i), ' ');
|
|
||||||
if (!in_array($tmp[0], $delimiters)) {
|
|
||||||
throw new ParseException(sprintf('Unexpected characters (%s).', substr($scalar, $i)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// "normal" string
|
|
||||||
if (!$delimiters) {
|
|
||||||
$output = substr($scalar, $i);
|
|
||||||
$i += strlen($output);
|
|
||||||
|
|
||||||
// remove comments
|
|
||||||
if (preg_match('/[ \t]+#/', $output, $match, PREG_OFFSET_CAPTURE)) {
|
|
||||||
$output = substr($output, 0, $match[0][1]);
|
|
||||||
}
|
|
||||||
} elseif (preg_match('/^(.+?)('.implode('|', $delimiters).')/', substr($scalar, $i), $match)) {
|
|
||||||
$output = $match[1];
|
|
||||||
$i += strlen($output);
|
|
||||||
} else {
|
|
||||||
throw new ParseException(sprintf('Malformed inline YAML string (%s).', $scalar));
|
|
||||||
}
|
|
||||||
|
|
||||||
// a non-quoted string cannot start with @ or ` (reserved) nor with a scalar indicator (| or >)
|
|
||||||
if ($output && ('@' === $output[0] || '`' === $output[0] || '|' === $output[0] || '>' === $output[0])) {
|
|
||||||
throw new ParseException(sprintf('The reserved indicator "%s" cannot start a plain scalar; you need to quote the scalar.', $output[0]));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($evaluate) {
|
|
||||||
$output = self::evaluateScalar($output, $references);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $output;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a quoted scalar to YAML.
|
|
||||||
*
|
|
||||||
* @param string $scalar
|
|
||||||
* @param int &$i
|
|
||||||
*
|
|
||||||
* @return string A YAML string
|
|
||||||
*
|
|
||||||
* @throws ParseException When malformed inline YAML string is parsed
|
|
||||||
*/
|
|
||||||
private static function parseQuotedScalar($scalar, &$i)
|
|
||||||
{
|
|
||||||
if (!preg_match('/'.self::REGEX_QUOTED_STRING.'/Au', substr($scalar, $i), $match)) {
|
|
||||||
throw new ParseException(sprintf('Malformed inline YAML string (%s).', substr($scalar, $i)));
|
|
||||||
}
|
|
||||||
|
|
||||||
$output = substr($match[0], 1, strlen($match[0]) - 2);
|
|
||||||
|
|
||||||
$unescaper = new Unescaper();
|
|
||||||
if ('"' == $scalar[$i]) {
|
|
||||||
$output = $unescaper->unescapeDoubleQuotedString($output);
|
|
||||||
} else {
|
|
||||||
$output = $unescaper->unescapeSingleQuotedString($output);
|
|
||||||
}
|
|
||||||
|
|
||||||
$i += strlen($match[0]);
|
|
||||||
|
|
||||||
return $output;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a sequence to a YAML string.
|
|
||||||
*
|
|
||||||
* @param string $sequence
|
|
||||||
* @param int &$i
|
|
||||||
* @param array $references
|
|
||||||
*
|
|
||||||
* @return string A YAML string
|
|
||||||
*
|
|
||||||
* @throws ParseException When malformed inline YAML string is parsed
|
|
||||||
*/
|
|
||||||
private static function parseSequence($sequence, &$i = 0, $references = array())
|
|
||||||
{
|
|
||||||
$output = array();
|
|
||||||
$len = strlen($sequence);
|
|
||||||
++$i;
|
|
||||||
|
|
||||||
// [foo, bar, ...]
|
|
||||||
while ($i < $len) {
|
|
||||||
switch ($sequence[$i]) {
|
|
||||||
case '[':
|
|
||||||
// nested sequence
|
|
||||||
$output[] = self::parseSequence($sequence, $i, $references);
|
|
||||||
break;
|
|
||||||
case '{':
|
|
||||||
// nested mapping
|
|
||||||
$output[] = self::parseMapping($sequence, $i, $references);
|
|
||||||
break;
|
|
||||||
case ']':
|
|
||||||
return $output;
|
|
||||||
case ',':
|
|
||||||
case ' ':
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$isQuoted = in_array($sequence[$i], array('"', "'"));
|
|
||||||
$value = self::parseScalar($sequence, array(',', ']'), array('"', "'"), $i, true, $references);
|
|
||||||
|
|
||||||
// the value can be an array if a reference has been resolved to an array var
|
|
||||||
if (!is_array($value) && !$isQuoted && false !== strpos($value, ': ')) {
|
|
||||||
// embedded mapping?
|
|
||||||
try {
|
|
||||||
$pos = 0;
|
|
||||||
$value = self::parseMapping('{'.$value.'}', $pos, $references);
|
|
||||||
} catch (\InvalidArgumentException $e) {
|
|
||||||
// no, it's not
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$output[] = $value;
|
|
||||||
|
|
||||||
--$i;
|
|
||||||
}
|
|
||||||
|
|
||||||
++$i;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ParseException(sprintf('Malformed inline YAML string %s', $sequence));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a mapping to a YAML string.
|
|
||||||
*
|
|
||||||
* @param string $mapping
|
|
||||||
* @param int &$i
|
|
||||||
* @param array $references
|
|
||||||
*
|
|
||||||
* @return string A YAML string
|
|
||||||
*
|
|
||||||
* @throws ParseException When malformed inline YAML string is parsed
|
|
||||||
*/
|
|
||||||
private static function parseMapping($mapping, &$i = 0, $references = array())
|
|
||||||
{
|
|
||||||
$output = array();
|
|
||||||
$len = strlen($mapping);
|
|
||||||
++$i;
|
|
||||||
|
|
||||||
// {foo: bar, bar:foo, ...}
|
|
||||||
while ($i < $len) {
|
|
||||||
switch ($mapping[$i]) {
|
|
||||||
case ' ':
|
|
||||||
case ',':
|
|
||||||
++$i;
|
|
||||||
continue 2;
|
|
||||||
case '}':
|
|
||||||
if (self::$objectForMap) {
|
|
||||||
return (object) $output;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $output;
|
|
||||||
}
|
|
||||||
|
|
||||||
// key
|
|
||||||
$key = self::parseScalar($mapping, array(':', ' '), array('"', "'"), $i, false);
|
|
||||||
|
|
||||||
// value
|
|
||||||
$done = false;
|
|
||||||
|
|
||||||
while ($i < $len) {
|
|
||||||
switch ($mapping[$i]) {
|
|
||||||
case '[':
|
|
||||||
// nested sequence
|
|
||||||
$value = self::parseSequence($mapping, $i, $references);
|
|
||||||
// Spec: Keys MUST be unique; first one wins.
|
|
||||||
// Parser cannot abort this mapping earlier, since lines
|
|
||||||
// are processed sequentially.
|
|
||||||
if (!isset($output[$key])) {
|
|
||||||
$output[$key] = $value;
|
|
||||||
}
|
|
||||||
$done = true;
|
|
||||||
break;
|
|
||||||
case '{':
|
|
||||||
// nested mapping
|
|
||||||
$value = self::parseMapping($mapping, $i, $references);
|
|
||||||
// Spec: Keys MUST be unique; first one wins.
|
|
||||||
// Parser cannot abort this mapping earlier, since lines
|
|
||||||
// are processed sequentially.
|
|
||||||
if (!isset($output[$key])) {
|
|
||||||
$output[$key] = $value;
|
|
||||||
}
|
|
||||||
$done = true;
|
|
||||||
break;
|
|
||||||
case ':':
|
|
||||||
case ' ':
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$value = self::parseScalar($mapping, array(',', '}'), array('"', "'"), $i, true, $references);
|
|
||||||
// Spec: Keys MUST be unique; first one wins.
|
|
||||||
// Parser cannot abort this mapping earlier, since lines
|
|
||||||
// are processed sequentially.
|
|
||||||
if (!isset($output[$key])) {
|
|
||||||
$output[$key] = $value;
|
|
||||||
}
|
|
||||||
$done = true;
|
|
||||||
--$i;
|
|
||||||
}
|
|
||||||
|
|
||||||
++$i;
|
|
||||||
|
|
||||||
if ($done) {
|
|
||||||
continue 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ParseException(sprintf('Malformed inline YAML string %s', $mapping));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluates scalars and replaces magic values.
|
|
||||||
*
|
|
||||||
* @param string $scalar
|
|
||||||
* @param array $references
|
|
||||||
*
|
|
||||||
* @return string A YAML string
|
|
||||||
*
|
|
||||||
* @throws ParseException when object parsing support was disabled and the parser detected a PHP object or when a reference could not be resolved
|
|
||||||
*/
|
|
||||||
private static function evaluateScalar($scalar, $references = array())
|
|
||||||
{
|
|
||||||
$scalar = trim($scalar);
|
|
||||||
$scalarLower = strtolower($scalar);
|
|
||||||
|
|
||||||
if (0 === strpos($scalar, '*')) {
|
|
||||||
if (false !== $pos = strpos($scalar, '#')) {
|
|
||||||
$value = substr($scalar, 1, $pos - 2);
|
|
||||||
} else {
|
|
||||||
$value = substr($scalar, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// an unquoted *
|
|
||||||
if (false === $value || '' === $value) {
|
|
||||||
throw new ParseException('A reference must contain at least one character.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!array_key_exists($value, $references)) {
|
|
||||||
throw new ParseException(sprintf('Reference "%s" does not exist.', $value));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $references[$value];
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (true) {
|
|
||||||
case 'null' === $scalarLower:
|
|
||||||
case '' === $scalar:
|
|
||||||
case '~' === $scalar:
|
|
||||||
return;
|
|
||||||
case 'true' === $scalarLower:
|
|
||||||
return true;
|
|
||||||
case 'false' === $scalarLower:
|
|
||||||
return false;
|
|
||||||
// Optimise for returning strings.
|
|
||||||
case $scalar[0] === '+' || $scalar[0] === '-' || $scalar[0] === '.' || $scalar[0] === '!' || is_numeric($scalar[0]):
|
|
||||||
switch (true) {
|
|
||||||
case 0 === strpos($scalar, '!str'):
|
|
||||||
return (string) substr($scalar, 5);
|
|
||||||
case 0 === strpos($scalar, '! '):
|
|
||||||
return (int) self::parseScalar(substr($scalar, 2));
|
|
||||||
case 0 === strpos($scalar, '!php/object:'):
|
|
||||||
if (self::$objectSupport) {
|
|
||||||
return unserialize(substr($scalar, 12));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self::$exceptionOnInvalidType) {
|
|
||||||
throw new ParseException('Object support when parsing a YAML file has been disabled.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
case 0 === strpos($scalar, '!!php/object:'):
|
|
||||||
if (self::$objectSupport) {
|
|
||||||
return unserialize(substr($scalar, 13));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self::$exceptionOnInvalidType) {
|
|
||||||
throw new ParseException('Object support when parsing a YAML file has been disabled.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
case 0 === strpos($scalar, '!!float '):
|
|
||||||
return (float) substr($scalar, 8);
|
|
||||||
case ctype_digit($scalar):
|
|
||||||
$raw = $scalar;
|
|
||||||
$cast = (int) $scalar;
|
|
||||||
|
|
||||||
return '0' == $scalar[0] ? octdec($scalar) : (((string) $raw == (string) $cast) ? $cast : $raw);
|
|
||||||
case '-' === $scalar[0] && ctype_digit(substr($scalar, 1)):
|
|
||||||
$raw = $scalar;
|
|
||||||
$cast = (int) $scalar;
|
|
||||||
|
|
||||||
return '0' == $scalar[1] ? octdec($scalar) : (((string) $raw === (string) $cast) ? $cast : $raw);
|
|
||||||
case is_numeric($scalar):
|
|
||||||
case preg_match(self::getHexRegex(), $scalar):
|
|
||||||
return '0x' === $scalar[0].$scalar[1] ? hexdec($scalar) : (float) $scalar;
|
|
||||||
case '.inf' === $scalarLower:
|
|
||||||
case '.nan' === $scalarLower:
|
|
||||||
return -log(0);
|
|
||||||
case '-.inf' === $scalarLower:
|
|
||||||
return log(0);
|
|
||||||
case preg_match('/^(-|\+)?[0-9,]+(\.[0-9]+)?$/', $scalar):
|
|
||||||
return (float) str_replace(',', '', $scalar);
|
|
||||||
case preg_match(self::getTimestampRegex(), $scalar):
|
|
||||||
$timeZone = date_default_timezone_get();
|
|
||||||
date_default_timezone_set('UTC');
|
|
||||||
$time = strtotime($scalar);
|
|
||||||
date_default_timezone_set($timeZone);
|
|
||||||
|
|
||||||
return $time;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return (string) $scalar;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a regex that matches a YAML date.
|
|
||||||
*
|
|
||||||
* @return string The regular expression
|
|
||||||
*
|
|
||||||
* @see http://www.yaml.org/spec/1.2/spec.html#id2761573
|
|
||||||
*/
|
|
||||||
private static function getTimestampRegex()
|
|
||||||
{
|
|
||||||
return <<<EOF
|
|
||||||
~^
|
|
||||||
(?P<year>[0-9][0-9][0-9][0-9])
|
|
||||||
-(?P<month>[0-9][0-9]?)
|
|
||||||
-(?P<day>[0-9][0-9]?)
|
|
||||||
(?:(?:[Tt]|[ \t]+)
|
|
||||||
(?P<hour>[0-9][0-9]?)
|
|
||||||
:(?P<minute>[0-9][0-9])
|
|
||||||
:(?P<second>[0-9][0-9])
|
|
||||||
(?:\.(?P<fraction>[0-9]*))?
|
|
||||||
(?:[ \t]*(?P<tz>Z|(?P<tz_sign>[-+])(?P<tz_hour>[0-9][0-9]?)
|
|
||||||
(?::(?P<tz_minute>[0-9][0-9]))?))?)?
|
|
||||||
$~x
|
|
||||||
EOF;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a regex that matches a YAML number in hexadecimal notation.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private static function getHexRegex()
|
|
||||||
{
|
|
||||||
return '~^0x[0-9a-f]++$~i';
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,141 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of the Symfony package.
|
|
||||||
*
|
|
||||||
* (c) Fabien Potencier <fabien@symfony.com>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace WapplerSystems\ABTest2\DeviceDetector\Yaml;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exception class thrown when an error occurs during parsing.
|
|
||||||
*
|
|
||||||
* @author Fabien Potencier <fabien@symfony.com>
|
|
||||||
*/
|
|
||||||
class ParseException extends \RuntimeException
|
|
||||||
{
|
|
||||||
private $parsedFile;
|
|
||||||
private $parsedLine;
|
|
||||||
private $snippet;
|
|
||||||
private $rawMessage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor.
|
|
||||||
*
|
|
||||||
* @param string $message The error message
|
|
||||||
* @param int $parsedLine The line where the error occurred
|
|
||||||
* @param int $snippet The snippet of code near the problem
|
|
||||||
* @param string $parsedFile The file name where the error occurred
|
|
||||||
* @param \Exception $previous The previous exception
|
|
||||||
*/
|
|
||||||
public function __construct($message, $parsedLine = -1, $snippet = null, $parsedFile = null, \Exception $previous = null)
|
|
||||||
{
|
|
||||||
$this->parsedFile = $parsedFile;
|
|
||||||
$this->parsedLine = $parsedLine;
|
|
||||||
$this->snippet = $snippet;
|
|
||||||
$this->rawMessage = $message;
|
|
||||||
|
|
||||||
$this->updateRepr();
|
|
||||||
|
|
||||||
parent::__construct($this->message, 0, $previous);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the snippet of code near the error.
|
|
||||||
*
|
|
||||||
* @return string The snippet of code
|
|
||||||
*/
|
|
||||||
public function getSnippet()
|
|
||||||
{
|
|
||||||
return $this->snippet;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the snippet of code near the error.
|
|
||||||
*
|
|
||||||
* @param string $snippet The code snippet
|
|
||||||
*/
|
|
||||||
public function setSnippet($snippet)
|
|
||||||
{
|
|
||||||
$this->snippet = $snippet;
|
|
||||||
|
|
||||||
$this->updateRepr();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the filename where the error occurred.
|
|
||||||
*
|
|
||||||
* This method returns null if a string is parsed.
|
|
||||||
*
|
|
||||||
* @return string The filename
|
|
||||||
*/
|
|
||||||
public function getParsedFile()
|
|
||||||
{
|
|
||||||
return $this->parsedFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the filename where the error occurred.
|
|
||||||
*
|
|
||||||
* @param string $parsedFile The filename
|
|
||||||
*/
|
|
||||||
public function setParsedFile($parsedFile)
|
|
||||||
{
|
|
||||||
$this->parsedFile = $parsedFile;
|
|
||||||
|
|
||||||
$this->updateRepr();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the line where the error occurred.
|
|
||||||
*
|
|
||||||
* @return int The file line
|
|
||||||
*/
|
|
||||||
public function getParsedLine()
|
|
||||||
{
|
|
||||||
return $this->parsedLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the line where the error occurred.
|
|
||||||
*
|
|
||||||
* @param int $parsedLine The file line
|
|
||||||
*/
|
|
||||||
public function setParsedLine($parsedLine)
|
|
||||||
{
|
|
||||||
$this->parsedLine = $parsedLine;
|
|
||||||
|
|
||||||
$this->updateRepr();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function updateRepr()
|
|
||||||
{
|
|
||||||
$this->message = $this->rawMessage;
|
|
||||||
|
|
||||||
$dot = false;
|
|
||||||
if ('.' === substr($this->message, -1)) {
|
|
||||||
$this->message = substr($this->message, 0, -1);
|
|
||||||
$dot = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null !== $this->parsedFile) {
|
|
||||||
$this->message .= sprintf(' in %s', json_encode($this->parsedFile, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->parsedLine >= 0) {
|
|
||||||
$this->message .= sprintf(' at line %d', $this->parsedLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->snippet) {
|
|
||||||
$this->message .= sprintf(' (near "%s")', $this->snippet);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($dot) {
|
|
||||||
$this->message .= '.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,777 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of the Symfony package.
|
|
||||||
*
|
|
||||||
* (c) Fabien Potencier <fabien@symfony.com>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace WapplerSystems\ABTest2\DeviceDetector\Yaml;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parser parses YAML strings to convert them to PHP arrays.
|
|
||||||
*
|
|
||||||
* @author Fabien Potencier <fabien@symfony.com>
|
|
||||||
*/
|
|
||||||
class Parser
|
|
||||||
{
|
|
||||||
const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
|
|
||||||
|
|
||||||
private $offset = 0;
|
|
||||||
private $lines = array();
|
|
||||||
private $currentLineNb = -1;
|
|
||||||
private $currentLine = '';
|
|
||||||
private $refs = array();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor.
|
|
||||||
*
|
|
||||||
* @param int $offset The offset of YAML document (used for line numbers in error messages)
|
|
||||||
*/
|
|
||||||
public function __construct($offset = 0)
|
|
||||||
{
|
|
||||||
$this->offset = $offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a YAML string to a PHP value.
|
|
||||||
*
|
|
||||||
* @param string $value A YAML string
|
|
||||||
* @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
|
|
||||||
* @param bool $objectSupport true if object support is enabled, false otherwise
|
|
||||||
* @param bool $objectForMap true if maps should return a stdClass instead of array()
|
|
||||||
*
|
|
||||||
* @return mixed A PHP value
|
|
||||||
*
|
|
||||||
* @throws ParseException If the YAML is not valid
|
|
||||||
*/
|
|
||||||
public function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false)
|
|
||||||
{
|
|
||||||
if (!preg_match('//u', $value)) {
|
|
||||||
throw new ParseException('The YAML value does not appear to be valid UTF-8.');
|
|
||||||
}
|
|
||||||
$this->currentLineNb = -1;
|
|
||||||
$this->currentLine = '';
|
|
||||||
$value = $this->cleanup($value);
|
|
||||||
$this->lines = explode("\n", $value);
|
|
||||||
|
|
||||||
if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) {
|
|
||||||
$mbEncoding = mb_internal_encoding();
|
|
||||||
mb_internal_encoding('UTF-8');
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = array();
|
|
||||||
$context = null;
|
|
||||||
$allowOverwrite = false;
|
|
||||||
while ($this->moveToNextLine()) {
|
|
||||||
if ($this->isCurrentLineEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// tab?
|
|
||||||
if ("\t" === $this->currentLine[0]) {
|
|
||||||
throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
$isRef = $mergeNode = false;
|
|
||||||
if (preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+?))?\s*$#u', $this->currentLine, $values)) {
|
|
||||||
if ($context && 'mapping' == $context) {
|
|
||||||
throw new ParseException('You cannot define a sequence item when in a mapping');
|
|
||||||
}
|
|
||||||
$context = 'sequence';
|
|
||||||
|
|
||||||
if (isset($values['value']) && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
|
|
||||||
$isRef = $matches['ref'];
|
|
||||||
$values['value'] = $matches['value'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// array
|
|
||||||
if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
|
|
||||||
$c = $this->getRealCurrentLineNb() + 1;
|
|
||||||
$parser = new self($c);
|
|
||||||
$parser->refs = &$this->refs;
|
|
||||||
$data[] = $parser->parse($this->getNextEmbedBlock(null, true), $exceptionOnInvalidType, $objectSupport, $objectForMap);
|
|
||||||
} else {
|
|
||||||
if (isset($values['leadspaces'])
|
|
||||||
&& preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $values['value'], $matches)
|
|
||||||
) {
|
|
||||||
// this is a compact notation element, add to next block and parse
|
|
||||||
$c = $this->getRealCurrentLineNb();
|
|
||||||
$parser = new self($c);
|
|
||||||
$parser->refs = &$this->refs;
|
|
||||||
|
|
||||||
$block = $values['value'];
|
|
||||||
if ($this->isNextLineIndented()) {
|
|
||||||
$block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + strlen($values['leadspaces']) + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$data[] = $parser->parse($block, $exceptionOnInvalidType, $objectSupport, $objectForMap);
|
|
||||||
} else {
|
|
||||||
$data[] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($isRef) {
|
|
||||||
$this->refs[$isRef] = end($data);
|
|
||||||
}
|
|
||||||
} elseif (preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $this->currentLine, $values) && (false === strpos($values['key'], ' #') || in_array($values['key'][0], array('"', "'")))) {
|
|
||||||
if ($context && 'sequence' == $context) {
|
|
||||||
throw new ParseException('You cannot define a mapping item when in a sequence');
|
|
||||||
}
|
|
||||||
$context = 'mapping';
|
|
||||||
|
|
||||||
// force correct settings
|
|
||||||
Inline::parse(null, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
|
|
||||||
try {
|
|
||||||
$key = Inline::parseScalar($values['key']);
|
|
||||||
} catch (ParseException $e) {
|
|
||||||
$e->setParsedLine($this->getRealCurrentLineNb() + 1);
|
|
||||||
$e->setSnippet($this->currentLine);
|
|
||||||
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert float keys to strings, to avoid being converted to integers by PHP
|
|
||||||
if (is_float($key)) {
|
|
||||||
$key = (string) $key;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('<<' === $key) {
|
|
||||||
$mergeNode = true;
|
|
||||||
$allowOverwrite = true;
|
|
||||||
if (isset($values['value']) && 0 === strpos($values['value'], '*')) {
|
|
||||||
$refName = substr($values['value'], 1);
|
|
||||||
if (!array_key_exists($refName, $this->refs)) {
|
|
||||||
throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
$refValue = $this->refs[$refName];
|
|
||||||
|
|
||||||
if (!is_array($refValue)) {
|
|
||||||
throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($refValue as $key => $value) {
|
|
||||||
if (!isset($data[$key])) {
|
|
||||||
$data[$key] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isset($values['value']) && $values['value'] !== '') {
|
|
||||||
$value = $values['value'];
|
|
||||||
} else {
|
|
||||||
$value = $this->getNextEmbedBlock();
|
|
||||||
}
|
|
||||||
$c = $this->getRealCurrentLineNb() + 1;
|
|
||||||
$parser = new self($c);
|
|
||||||
$parser->refs = &$this->refs;
|
|
||||||
$parsed = $parser->parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
|
|
||||||
|
|
||||||
if (!is_array($parsed)) {
|
|
||||||
throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($parsed[0])) {
|
|
||||||
// If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
|
|
||||||
// and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
|
|
||||||
// in the sequence override keys specified in later mapping nodes.
|
|
||||||
foreach ($parsed as $parsedItem) {
|
|
||||||
if (!is_array($parsedItem)) {
|
|
||||||
throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($parsedItem as $key => $value) {
|
|
||||||
if (!isset($data[$key])) {
|
|
||||||
$data[$key] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
|
|
||||||
// current mapping, unless the key already exists in it.
|
|
||||||
foreach ($parsed as $key => $value) {
|
|
||||||
if (!isset($data[$key])) {
|
|
||||||
$data[$key] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} elseif (isset($values['value']) && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
|
|
||||||
$isRef = $matches['ref'];
|
|
||||||
$values['value'] = $matches['value'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($mergeNode) {
|
|
||||||
// Merge keys
|
|
||||||
} elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
|
|
||||||
// hash
|
|
||||||
// if next line is less indented or equal, then it means that the current value is null
|
|
||||||
if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
|
|
||||||
// Spec: Keys MUST be unique; first one wins.
|
|
||||||
// But overwriting is allowed when a merge node is used in current block.
|
|
||||||
if ($allowOverwrite || !isset($data[$key])) {
|
|
||||||
$data[$key] = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$c = $this->getRealCurrentLineNb() + 1;
|
|
||||||
$parser = new self($c);
|
|
||||||
$parser->refs = &$this->refs;
|
|
||||||
$value = $parser->parse($this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap);
|
|
||||||
// Spec: Keys MUST be unique; first one wins.
|
|
||||||
// But overwriting is allowed when a merge node is used in current block.
|
|
||||||
if ($allowOverwrite || !isset($data[$key])) {
|
|
||||||
$data[$key] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context);
|
|
||||||
// Spec: Keys MUST be unique; first one wins.
|
|
||||||
// But overwriting is allowed when a merge node is used in current block.
|
|
||||||
if ($allowOverwrite || !isset($data[$key])) {
|
|
||||||
$data[$key] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($isRef) {
|
|
||||||
$this->refs[$isRef] = $data[$key];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// multiple documents are not supported
|
|
||||||
if ('---' === $this->currentLine) {
|
|
||||||
throw new ParseException('Multiple documents are not supported.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1-liner optionally followed by newline(s)
|
|
||||||
if (is_string($value) && $this->lines[0] === trim($value)) {
|
|
||||||
try {
|
|
||||||
$value = Inline::parse($this->lines[0], $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
|
|
||||||
} catch (ParseException $e) {
|
|
||||||
$e->setParsedLine($this->getRealCurrentLineNb() + 1);
|
|
||||||
$e->setSnippet($this->currentLine);
|
|
||||||
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_array($value)) {
|
|
||||||
$first = reset($value);
|
|
||||||
if (is_string($first) && 0 === strpos($first, '*')) {
|
|
||||||
$data = array();
|
|
||||||
foreach ($value as $alias) {
|
|
||||||
$data[] = $this->refs[substr($alias, 1)];
|
|
||||||
}
|
|
||||||
$value = $data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($mbEncoding)) {
|
|
||||||
mb_internal_encoding($mbEncoding);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (preg_last_error()) {
|
|
||||||
case PREG_INTERNAL_ERROR:
|
|
||||||
$error = 'Internal PCRE error.';
|
|
||||||
break;
|
|
||||||
case PREG_BACKTRACK_LIMIT_ERROR:
|
|
||||||
$error = 'pcre.backtrack_limit reached.';
|
|
||||||
break;
|
|
||||||
case PREG_RECURSION_LIMIT_ERROR:
|
|
||||||
$error = 'pcre.recursion_limit reached.';
|
|
||||||
break;
|
|
||||||
case PREG_BAD_UTF8_ERROR:
|
|
||||||
$error = 'Malformed UTF-8 data.';
|
|
||||||
break;
|
|
||||||
case PREG_BAD_UTF8_OFFSET_ERROR:
|
|
||||||
$error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$error = 'Unable to parse.';
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ParseException($error, $this->getRealCurrentLineNb() + 1, $this->currentLine);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($mbEncoding)) {
|
|
||||||
mb_internal_encoding($mbEncoding);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($objectForMap && !is_object($data) && 'mapping' === $context) {
|
|
||||||
$object = new \stdClass();
|
|
||||||
|
|
||||||
foreach ($data as $key => $value) {
|
|
||||||
$object->$key = $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = $object;
|
|
||||||
}
|
|
||||||
|
|
||||||
return empty($data) ? null : $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current line number (takes the offset into account).
|
|
||||||
*
|
|
||||||
* @return int The current line number
|
|
||||||
*/
|
|
||||||
private function getRealCurrentLineNb()
|
|
||||||
{
|
|
||||||
return $this->currentLineNb + $this->offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current line indentation.
|
|
||||||
*
|
|
||||||
* @return int The current line indentation
|
|
||||||
*/
|
|
||||||
private function getCurrentLineIndentation()
|
|
||||||
{
|
|
||||||
return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' '));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the next embed block of YAML.
|
|
||||||
*
|
|
||||||
* @param int $indentation The indent level at which the block is to be read, or null for default
|
|
||||||
* @param bool $inSequence True if the enclosing data structure is a sequence
|
|
||||||
*
|
|
||||||
* @return string A YAML string
|
|
||||||
*
|
|
||||||
* @throws ParseException When indentation problem are detected
|
|
||||||
*/
|
|
||||||
private function getNextEmbedBlock($indentation = null, $inSequence = false)
|
|
||||||
{
|
|
||||||
$oldLineIndentation = $this->getCurrentLineIndentation();
|
|
||||||
$blockScalarIndentations = array();
|
|
||||||
|
|
||||||
if ($this->isBlockScalarHeader()) {
|
|
||||||
$blockScalarIndentations[] = $this->getCurrentLineIndentation();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->moveToNextLine()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null === $indentation) {
|
|
||||||
$newIndent = $this->getCurrentLineIndentation();
|
|
||||||
|
|
||||||
$unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem();
|
|
||||||
|
|
||||||
if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
|
|
||||||
throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$newIndent = $indentation;
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = array();
|
|
||||||
if ($this->getCurrentLineIndentation() >= $newIndent) {
|
|
||||||
$data[] = substr($this->currentLine, $newIndent);
|
|
||||||
} else {
|
|
||||||
$this->moveToPreviousLine();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) {
|
|
||||||
// the previous line contained a dash but no item content, this line is a sequence item with the same indentation
|
|
||||||
// and therefore no nested list or mapping
|
|
||||||
$this->moveToPreviousLine();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
|
|
||||||
|
|
||||||
if (empty($blockScalarIndentations) && $this->isBlockScalarHeader()) {
|
|
||||||
$blockScalarIndentations[] = $this->getCurrentLineIndentation();
|
|
||||||
}
|
|
||||||
|
|
||||||
$previousLineIndentation = $this->getCurrentLineIndentation();
|
|
||||||
|
|
||||||
while ($this->moveToNextLine()) {
|
|
||||||
$indent = $this->getCurrentLineIndentation();
|
|
||||||
|
|
||||||
// terminate all block scalars that are more indented than the current line
|
|
||||||
if (!empty($blockScalarIndentations) && $indent < $previousLineIndentation && trim($this->currentLine) !== '') {
|
|
||||||
foreach ($blockScalarIndentations as $key => $blockScalarIndentation) {
|
|
||||||
if ($blockScalarIndentation >= $this->getCurrentLineIndentation()) {
|
|
||||||
unset($blockScalarIndentations[$key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($blockScalarIndentations) && !$this->isCurrentLineComment() && $this->isBlockScalarHeader()) {
|
|
||||||
$blockScalarIndentations[] = $this->getCurrentLineIndentation();
|
|
||||||
}
|
|
||||||
|
|
||||||
$previousLineIndentation = $indent;
|
|
||||||
|
|
||||||
if ($isItUnindentedCollection && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
|
|
||||||
$this->moveToPreviousLine();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->isCurrentLineBlank()) {
|
|
||||||
$data[] = substr($this->currentLine, $newIndent);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we ignore "comment" lines only when we are not inside a scalar block
|
|
||||||
if (empty($blockScalarIndentations) && $this->isCurrentLineComment()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($indent >= $newIndent) {
|
|
||||||
$data[] = substr($this->currentLine, $newIndent);
|
|
||||||
} elseif (0 == $indent) {
|
|
||||||
$this->moveToPreviousLine();
|
|
||||||
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return implode("\n", $data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Moves the parser to the next line.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
private function moveToNextLine()
|
|
||||||
{
|
|
||||||
if ($this->currentLineNb >= count($this->lines) - 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->currentLine = $this->lines[++$this->currentLineNb];
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Moves the parser to the previous line.
|
|
||||||
*/
|
|
||||||
private function moveToPreviousLine()
|
|
||||||
{
|
|
||||||
$this->currentLine = $this->lines[--$this->currentLineNb];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a YAML value.
|
|
||||||
*
|
|
||||||
* @param string $value A YAML value
|
|
||||||
* @param bool $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise
|
|
||||||
* @param bool $objectSupport True if object support is enabled, false otherwise
|
|
||||||
* @param bool $objectForMap true if maps should return a stdClass instead of array()
|
|
||||||
* @param string $context The parser context (either sequence or mapping)
|
|
||||||
*
|
|
||||||
* @return mixed A PHP value
|
|
||||||
*
|
|
||||||
* @throws ParseException When reference does not exist
|
|
||||||
*/
|
|
||||||
private function parseValue($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $context)
|
|
||||||
{
|
|
||||||
if (0 === strpos($value, '*')) {
|
|
||||||
if (false !== $pos = strpos($value, '#')) {
|
|
||||||
$value = substr($value, 1, $pos - 2);
|
|
||||||
} else {
|
|
||||||
$value = substr($value, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!array_key_exists($value, $this->refs)) {
|
|
||||||
throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->refs[$value];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preg_match('/^'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
|
|
||||||
$modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
|
|
||||||
|
|
||||||
return $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$parsedValue = Inline::parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
|
|
||||||
|
|
||||||
if ('mapping' === $context && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) {
|
|
||||||
throw new ParseException('A colon cannot be used in an unquoted mapping value.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $parsedValue;
|
|
||||||
} catch (ParseException $e) {
|
|
||||||
$e->setParsedLine($this->getRealCurrentLineNb() + 1);
|
|
||||||
$e->setSnippet($this->currentLine);
|
|
||||||
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a block scalar.
|
|
||||||
*
|
|
||||||
* @param string $style The style indicator that was used to begin this block scalar (| or >)
|
|
||||||
* @param string $chomping The chomping indicator that was used to begin this block scalar (+ or -)
|
|
||||||
* @param int $indentation The indentation indicator that was used to begin this block scalar
|
|
||||||
*
|
|
||||||
* @return string The text value
|
|
||||||
*/
|
|
||||||
private function parseBlockScalar($style, $chomping = '', $indentation = 0)
|
|
||||||
{
|
|
||||||
$notEOF = $this->moveToNextLine();
|
|
||||||
if (!$notEOF) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$isCurrentLineBlank = $this->isCurrentLineBlank();
|
|
||||||
$blockLines = array();
|
|
||||||
|
|
||||||
// leading blank lines are consumed before determining indentation
|
|
||||||
while ($notEOF && $isCurrentLineBlank) {
|
|
||||||
// newline only if not EOF
|
|
||||||
if ($notEOF = $this->moveToNextLine()) {
|
|
||||||
$blockLines[] = '';
|
|
||||||
$isCurrentLineBlank = $this->isCurrentLineBlank();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// determine indentation if not specified
|
|
||||||
if (0 === $indentation) {
|
|
||||||
if (preg_match('/^ +/', $this->currentLine, $matches)) {
|
|
||||||
$indentation = strlen($matches[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($indentation > 0) {
|
|
||||||
$pattern = sprintf('/^ {%d}(.*)$/', $indentation);
|
|
||||||
|
|
||||||
while (
|
|
||||||
$notEOF && (
|
|
||||||
$isCurrentLineBlank ||
|
|
||||||
preg_match($pattern, $this->currentLine, $matches)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
if ($isCurrentLineBlank && strlen($this->currentLine) > $indentation) {
|
|
||||||
$blockLines[] = substr($this->currentLine, $indentation);
|
|
||||||
} elseif ($isCurrentLineBlank) {
|
|
||||||
$blockLines[] = '';
|
|
||||||
} else {
|
|
||||||
$blockLines[] = $matches[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// newline only if not EOF
|
|
||||||
if ($notEOF = $this->moveToNextLine()) {
|
|
||||||
$isCurrentLineBlank = $this->isCurrentLineBlank();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} elseif ($notEOF) {
|
|
||||||
$blockLines[] = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($notEOF) {
|
|
||||||
$blockLines[] = '';
|
|
||||||
$this->moveToPreviousLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
// folded style
|
|
||||||
if ('>' === $style) {
|
|
||||||
$text = '';
|
|
||||||
$previousLineIndented = false;
|
|
||||||
$previousLineBlank = false;
|
|
||||||
|
|
||||||
for ($i = 0; $i < count($blockLines); ++$i) {
|
|
||||||
if ('' === $blockLines[$i]) {
|
|
||||||
$text .= "\n";
|
|
||||||
$previousLineIndented = false;
|
|
||||||
$previousLineBlank = true;
|
|
||||||
} elseif (' ' === $blockLines[$i][0]) {
|
|
||||||
$text .= "\n".$blockLines[$i];
|
|
||||||
$previousLineIndented = true;
|
|
||||||
$previousLineBlank = false;
|
|
||||||
} elseif ($previousLineIndented) {
|
|
||||||
$text .= "\n".$blockLines[$i];
|
|
||||||
$previousLineIndented = false;
|
|
||||||
$previousLineBlank = false;
|
|
||||||
} elseif ($previousLineBlank || 0 === $i) {
|
|
||||||
$text .= $blockLines[$i];
|
|
||||||
$previousLineIndented = false;
|
|
||||||
$previousLineBlank = false;
|
|
||||||
} else {
|
|
||||||
$text .= ' '.$blockLines[$i];
|
|
||||||
$previousLineIndented = false;
|
|
||||||
$previousLineBlank = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$text = implode("\n", $blockLines);
|
|
||||||
}
|
|
||||||
|
|
||||||
// deal with trailing newlines
|
|
||||||
if ('' === $chomping) {
|
|
||||||
$text = preg_replace('/\n+$/', "\n", $text);
|
|
||||||
} elseif ('-' === $chomping) {
|
|
||||||
$text = preg_replace('/\n+$/', '', $text);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the next line is indented.
|
|
||||||
*
|
|
||||||
* @return bool Returns true if the next line is indented, false otherwise
|
|
||||||
*/
|
|
||||||
private function isNextLineIndented()
|
|
||||||
{
|
|
||||||
$currentIndentation = $this->getCurrentLineIndentation();
|
|
||||||
$EOF = !$this->moveToNextLine();
|
|
||||||
|
|
||||||
while (!$EOF && $this->isCurrentLineEmpty()) {
|
|
||||||
$EOF = !$this->moveToNextLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($EOF) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$ret = false;
|
|
||||||
if ($this->getCurrentLineIndentation() > $currentIndentation) {
|
|
||||||
$ret = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->moveToPreviousLine();
|
|
||||||
|
|
||||||
return $ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the current line is blank or if it is a comment line.
|
|
||||||
*
|
|
||||||
* @return bool Returns true if the current line is empty or if it is a comment line, false otherwise
|
|
||||||
*/
|
|
||||||
private function isCurrentLineEmpty()
|
|
||||||
{
|
|
||||||
return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the current line is blank.
|
|
||||||
*
|
|
||||||
* @return bool Returns true if the current line is blank, false otherwise
|
|
||||||
*/
|
|
||||||
private function isCurrentLineBlank()
|
|
||||||
{
|
|
||||||
return '' == trim($this->currentLine, ' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the current line is a comment line.
|
|
||||||
*
|
|
||||||
* @return bool Returns true if the current line is a comment line, false otherwise
|
|
||||||
*/
|
|
||||||
private function isCurrentLineComment()
|
|
||||||
{
|
|
||||||
//checking explicitly the first char of the trim is faster than loops or strpos
|
|
||||||
$ltrimmedLine = ltrim($this->currentLine, ' ');
|
|
||||||
|
|
||||||
return '' !== $ltrimmedLine && $ltrimmedLine[0] === '#';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanups a YAML string to be parsed.
|
|
||||||
*
|
|
||||||
* @param string $value The input YAML string
|
|
||||||
*
|
|
||||||
* @return string A cleaned up YAML string
|
|
||||||
*/
|
|
||||||
private function cleanup($value)
|
|
||||||
{
|
|
||||||
$value = str_replace(array("\r\n", "\r"), "\n", $value);
|
|
||||||
|
|
||||||
// strip YAML header
|
|
||||||
$count = 0;
|
|
||||||
$value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count);
|
|
||||||
$this->offset += $count;
|
|
||||||
|
|
||||||
// remove leading comments
|
|
||||||
$trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
|
|
||||||
if ($count == 1) {
|
|
||||||
// items have been removed, update the offset
|
|
||||||
$this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
|
|
||||||
$value = $trimmedValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove start of the document marker (---)
|
|
||||||
$trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
|
|
||||||
if ($count == 1) {
|
|
||||||
// items have been removed, update the offset
|
|
||||||
$this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
|
|
||||||
$value = $trimmedValue;
|
|
||||||
|
|
||||||
// remove end of the document marker (...)
|
|
||||||
$value = preg_replace('#\.\.\.\s*$#', '', $value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the next line starts unindented collection.
|
|
||||||
*
|
|
||||||
* @return bool Returns true if the next line starts unindented collection, false otherwise
|
|
||||||
*/
|
|
||||||
private function isNextLineUnIndentedCollection()
|
|
||||||
{
|
|
||||||
$currentIndentation = $this->getCurrentLineIndentation();
|
|
||||||
$notEOF = $this->moveToNextLine();
|
|
||||||
|
|
||||||
while ($notEOF && $this->isCurrentLineEmpty()) {
|
|
||||||
$notEOF = $this->moveToNextLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (false === $notEOF) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$ret = false;
|
|
||||||
if (
|
|
||||||
$this->getCurrentLineIndentation() == $currentIndentation
|
|
||||||
&&
|
|
||||||
$this->isStringUnIndentedCollectionItem()
|
|
||||||
) {
|
|
||||||
$ret = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->moveToPreviousLine();
|
|
||||||
|
|
||||||
return $ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the string is un-indented collection item.
|
|
||||||
*
|
|
||||||
* @return bool Returns true if the string is un-indented collection item, false otherwise
|
|
||||||
*/
|
|
||||||
private function isStringUnIndentedCollectionItem()
|
|
||||||
{
|
|
||||||
return 0 === strpos($this->currentLine, '- ');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests whether or not the current line is the header of a block scalar.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
private function isBlockScalarHeader()
|
|
||||||
{
|
|
||||||
return (bool) preg_match('~'.self::BLOCK_SCALAR_HEADER_PATTERN.'$~', $this->currentLine);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,141 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of the Symfony package.
|
|
||||||
*
|
|
||||||
* (c) Fabien Potencier <fabien@symfony.com>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace WapplerSystems\ABTest2\DeviceDetector\Yaml;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unescaper encapsulates unescaping rules for single and double-quoted
|
|
||||||
* YAML strings.
|
|
||||||
*
|
|
||||||
* @author Matthew Lewinski <matthew@lewinski.org>
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
class Unescaper
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Regex fragment that matches an escaped character in a double quoted string.
|
|
||||||
*/
|
|
||||||
const REGEX_ESCAPED_CHARACTER = '\\\\(x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|.)';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unescapes a single quoted string.
|
|
||||||
*
|
|
||||||
* @param string $value A single quoted string.
|
|
||||||
*
|
|
||||||
* @return string The unescaped string.
|
|
||||||
*/
|
|
||||||
public function unescapeSingleQuotedString($value)
|
|
||||||
{
|
|
||||||
return str_replace('\'\'', '\'', $value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unescapes a double quoted string.
|
|
||||||
*
|
|
||||||
* @param string $value A double quoted string.
|
|
||||||
*
|
|
||||||
* @return string The unescaped string.
|
|
||||||
*/
|
|
||||||
public function unescapeDoubleQuotedString($value)
|
|
||||||
{
|
|
||||||
$callback = function ($match) {
|
|
||||||
return $this->unescapeCharacter($match[0]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// evaluate the string
|
|
||||||
return preg_replace_callback('/'.self::REGEX_ESCAPED_CHARACTER.'/u', $callback, $value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unescapes a character that was found in a double-quoted string.
|
|
||||||
*
|
|
||||||
* @param string $value An escaped character
|
|
||||||
*
|
|
||||||
* @return string The unescaped character
|
|
||||||
*/
|
|
||||||
private function unescapeCharacter($value)
|
|
||||||
{
|
|
||||||
switch ($value[1]) {
|
|
||||||
case '0':
|
|
||||||
return "\x0";
|
|
||||||
case 'a':
|
|
||||||
return "\x7";
|
|
||||||
case 'b':
|
|
||||||
return "\x8";
|
|
||||||
case 't':
|
|
||||||
return "\t";
|
|
||||||
case "\t":
|
|
||||||
return "\t";
|
|
||||||
case 'n':
|
|
||||||
return "\n";
|
|
||||||
case 'v':
|
|
||||||
return "\xB";
|
|
||||||
case 'f':
|
|
||||||
return "\xC";
|
|
||||||
case 'r':
|
|
||||||
return "\r";
|
|
||||||
case 'e':
|
|
||||||
return "\x1B";
|
|
||||||
case ' ':
|
|
||||||
return ' ';
|
|
||||||
case '"':
|
|
||||||
return '"';
|
|
||||||
case '/':
|
|
||||||
return '/';
|
|
||||||
case '\\':
|
|
||||||
return '\\';
|
|
||||||
case 'N':
|
|
||||||
// U+0085 NEXT LINE
|
|
||||||
return "\xC2\x85";
|
|
||||||
case '_':
|
|
||||||
// U+00A0 NO-BREAK SPACE
|
|
||||||
return "\xC2\xA0";
|
|
||||||
case 'L':
|
|
||||||
// U+2028 LINE SEPARATOR
|
|
||||||
return "\xE2\x80\xA8";
|
|
||||||
case 'P':
|
|
||||||
// U+2029 PARAGRAPH SEPARATOR
|
|
||||||
return "\xE2\x80\xA9";
|
|
||||||
case 'x':
|
|
||||||
return self::utf8chr(hexdec(substr($value, 2, 2)));
|
|
||||||
case 'u':
|
|
||||||
return self::utf8chr(hexdec(substr($value, 2, 4)));
|
|
||||||
case 'U':
|
|
||||||
return self::utf8chr(hexdec(substr($value, 2, 8)));
|
|
||||||
default:
|
|
||||||
throw new ParseException(sprintf('Found unknown escape character "%s".', $value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the UTF-8 character for the given code point.
|
|
||||||
*
|
|
||||||
* @param int $c The unicode code point
|
|
||||||
*
|
|
||||||
* @return string The corresponding UTF-8 character
|
|
||||||
*/
|
|
||||||
private static function utf8chr($c)
|
|
||||||
{
|
|
||||||
if (0x80 > $c %= 0x200000) {
|
|
||||||
return chr($c);
|
|
||||||
}
|
|
||||||
if (0x800 > $c) {
|
|
||||||
return chr(0xC0 | $c >> 6).chr(0x80 | $c & 0x3F);
|
|
||||||
}
|
|
||||||
if (0x10000 > $c) {
|
|
||||||
return chr(0xE0 | $c >> 12).chr(0x80 | $c >> 6 & 0x3F).chr(0x80 | $c & 0x3F);
|
|
||||||
}
|
|
||||||
|
|
||||||
return chr(0xF0 | $c >> 18).chr(0x80 | $c >> 12 & 0x3F).chr(0x80 | $c >> 6 & 0x3F).chr(0x80 | $c & 0x3F);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,148 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace WapplerSystems\ABTest2;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This file is part of the "abtest2" Extension for TYPO3 CMS.
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please read the
|
|
||||||
* LICENSE.txt file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
||||||
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
|
|
||||||
use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
|
|
||||||
use TYPO3\CMS\Frontend\Page\PageRepository;
|
|
||||||
use WapplerSystems\ABTest2\DeviceDetector\DeviceDetector;
|
|
||||||
use WapplerSystems\ABTest2\DeviceDetector\Yaml\Parser;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class detects which page version (either by cookie or by random) and sets the page content ID accordingly.
|
|
||||||
*
|
|
||||||
* @package WapplerSystems\ABTest2
|
|
||||||
* @author Sven Wapler <typo3YYYYY@wappler.systems>
|
|
||||||
*/
|
|
||||||
class Helper
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param array $params
|
|
||||||
* @param $tsFeController TypoScriptFrontendController
|
|
||||||
* @return void
|
|
||||||
* @throws \InvalidArgumentException
|
|
||||||
*/
|
|
||||||
public function determineContentId(array $params, &$tsFeController)
|
|
||||||
{
|
|
||||||
$deviceDetector = new DeviceDetector();
|
|
||||||
$deviceDetector->setUserAgent($_SERVER['HTTP_USER_AGENT']);
|
|
||||||
try {
|
|
||||||
$deviceDetector->setYamlParser(new Parser());
|
|
||||||
$deviceDetector->parse();
|
|
||||||
if ($deviceDetector->isBot()) return;
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
}
|
|
||||||
|
|
||||||
$currentPageId = $targetPageId = $tsFeController->id;
|
|
||||||
|
|
||||||
// Get the rootpage_id
|
|
||||||
$pageRepository = GeneralUtility::makeInstance(PageRepository::class);
|
|
||||||
$rootpage_id = array_pop($pageRepository->getRootLine($GLOBALS['TSFE']->id));
|
|
||||||
|
|
||||||
// If the ID is NULL, then we set this value to the rootpage_id. NULL is the "Home"page, ID is a specific sub-page, e.g. www.domain.de (NULL) - www.domain.de/page.html (ID)
|
|
||||||
if (!$currentPageId) {
|
|
||||||
if ($rootpage_id) {
|
|
||||||
$currentPageId = $rootpage_id;
|
|
||||||
} else {
|
|
||||||
// Leave the function because we can not determine the ID.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$currentPagePropertiesArray = $pageRepository->getPage($currentPageId);
|
|
||||||
|
|
||||||
$pageBPageId = $currentPagePropertiesArray['tx_abtest2_b_id'];
|
|
||||||
/* TODO: check if page b exists */
|
|
||||||
$cookieLifeTime = $currentPagePropertiesArray['tx_abtest2_cookie_time'];
|
|
||||||
|
|
||||||
if ($pageBPageId) {
|
|
||||||
|
|
||||||
$pageBPropertiesArray = $pageRepository->getPage($pageBPageId);
|
|
||||||
$cookieValue = $_COOKIE['abtest2'];
|
|
||||||
|
|
||||||
if ($cookieValue === 'b') {
|
|
||||||
$targetPageId = $pageBPageId;
|
|
||||||
$currentPagePropertiesArray = $pageBPropertiesArray;
|
|
||||||
} else if ($cookieValue === 'a') {
|
|
||||||
|
|
||||||
} else {
|
|
||||||
$cookieValue = 'a';
|
|
||||||
/* select least used page */
|
|
||||||
|
|
||||||
if ((int)$currentPagePropertiesArray['tx_abtest2_counter'] > (int)$pageBPropertiesArray['tx_abtest2_counter']) {
|
|
||||||
/* take b */
|
|
||||||
$targetPageId = $pageBPageId;
|
|
||||||
$currentPagePropertiesArray = $pageBPropertiesArray;
|
|
||||||
$cookieValue = 'b';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* rise counter */
|
|
||||||
$GLOBALS['TYPO3_DB']->exec_UPDATEquery('pages', 'uid='. (int)$targetPageId, array('tx_abtest2_counter' => $currentPagePropertiesArray['tx_abtest2_counter'] + 1));
|
|
||||||
|
|
||||||
setcookie('abtest2', $cookieValue, time() + $cookieLifeTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If current page ID is different from the random page ID we set the correct page ID.
|
|
||||||
if ($currentPageId !== $targetPageId) {
|
|
||||||
$tsFeController->contentPid = $targetPageId;
|
|
||||||
$tsFeController->page['content_from_pid'] = $targetPageId;
|
|
||||||
}
|
|
||||||
|
|
||||||
$_GET['abtest'] = $cookieValue;
|
|
||||||
|
|
||||||
$this->makeCacheHash($tsFeController);
|
|
||||||
|
|
||||||
|
|
||||||
if ($currentPagePropertiesArray) {
|
|
||||||
$additionalHeaderData = $currentPagePropertiesArray['tx_abtest2_header'];
|
|
||||||
$additionalFooterData = $currentPagePropertiesArray['tx_abtest2_footer'];
|
|
||||||
if ($additionalHeaderData) {
|
|
||||||
$tsFeController->additionalHeaderData['abtest2'] = $additionalHeaderData;
|
|
||||||
}
|
|
||||||
if ($additionalFooterData) {
|
|
||||||
$tsFeController->additionalFooterData['abtest2'] = $additionalFooterData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param $tsFeController TypoScriptFrontendController
|
|
||||||
* @return void
|
|
||||||
* @throws \InvalidArgumentException
|
|
||||||
*/
|
|
||||||
private function makeCacheHash(&$tsFeController)
|
|
||||||
{
|
|
||||||
$GET = GeneralUtility::_GET();
|
|
||||||
|
|
||||||
/* Fix for root pages */
|
|
||||||
if (!isset($GET['id'])) {
|
|
||||||
$GET['id'] = $tsFeController->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var CacheHashCalculator $cacheHash */
|
|
||||||
$cacheHash = GeneralUtility::makeInstance(CacheHashCalculator::class);
|
|
||||||
|
|
||||||
$tsFeController->cHash_array = $cacheHash->getRelevantParameters(GeneralUtility::implodeArrayForUrl('', $GET));
|
|
||||||
$tsFeController->cHash = $cacheHash->calculateCacheHash($tsFeController->cHash_array);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
65
Classes/Middleware/SetCookie.php
Normal file
65
Classes/Middleware/SetCookie.php
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2023 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WerkraumMedia\ABTest\Middleware;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Cookie as SymfonyCookie;
|
||||||
|
use WerkraumMedia\ABTest\Cookie;
|
||||||
|
|
||||||
|
class SetCookie implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Cookie
|
||||||
|
*/
|
||||||
|
private $cookie;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
Cookie $cookie
|
||||||
|
) {
|
||||||
|
$this->cookie = $cookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process(
|
||||||
|
ServerRequestInterface $request,
|
||||||
|
RequestHandlerInterface $handler
|
||||||
|
): ResponseInterface {
|
||||||
|
$response = $handler->handle($request);
|
||||||
|
|
||||||
|
if ($this->cookie->needsToBeSet()) {
|
||||||
|
$cookie = new SymfonyCookie(
|
||||||
|
'ab-' . $this->cookie->getRequestedPage(),
|
||||||
|
(string)$this->cookie->getActualPage(),
|
||||||
|
time() + $this->cookie->getLifetime()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Do we need to adjust response header for caching to not cache the cookie within proxies?
|
||||||
|
$response = $response->withAddedHeader('Set-Cookie', $cookie->__toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
64
Classes/PageRepository.php
Normal file
64
Classes/PageRepository.php
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2023 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WerkraumMedia\ABTest;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
|
||||||
|
class PageRepository
|
||||||
|
{
|
||||||
|
private ConnectionPool $connectionPool;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
ConnectionPool $connectionPool
|
||||||
|
) {
|
||||||
|
$this->connectionPool = $connectionPool;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPage(int $uid): array
|
||||||
|
{
|
||||||
|
$result = $this->connectionPool->getConnectionForTable('pages')
|
||||||
|
->select(
|
||||||
|
['*'],
|
||||||
|
'pages',
|
||||||
|
['uid' => $uid]
|
||||||
|
)
|
||||||
|
->fetchAssociative()
|
||||||
|
;
|
||||||
|
|
||||||
|
if (is_array($result) === false) {
|
||||||
|
$result = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateCounter(int $uid, int $counter): void
|
||||||
|
{
|
||||||
|
$this->connectionPool->getConnectionForTable('pages')->update(
|
||||||
|
'pages',
|
||||||
|
['tx_abtest_counter' => $counter],
|
||||||
|
['uid' => $uid]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
149
Classes/Switcher.php
Normal file
149
Classes/Switcher.php
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2023 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WerkraumMedia\ABTest;
|
||||||
|
|
||||||
|
use DeviceDetector\DeviceDetector;
|
||||||
|
use TYPO3\CMS\Core\Http\ServerRequest;
|
||||||
|
use TYPO3\CMS\Core\Site\Entity\Site;
|
||||||
|
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will decide whether to switch to another variant.
|
||||||
|
*/
|
||||||
|
class Switcher
|
||||||
|
{
|
||||||
|
private PageRepository $pageRepository;
|
||||||
|
|
||||||
|
private Cookie $cookie;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
PageRepository $pageRepository,
|
||||||
|
Cookie $cookie
|
||||||
|
) {
|
||||||
|
$this->pageRepository = $pageRepository;
|
||||||
|
$this->cookie = $cookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function determineContentId(
|
||||||
|
array $params,
|
||||||
|
TypoScriptFrontendController $frontendController
|
||||||
|
): void {
|
||||||
|
if ($this->isRequestByBot()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentPageId = $frontendController->id;
|
||||||
|
if (is_numeric($currentPageId) === false) {
|
||||||
|
$currentPageId = $this->getRootPageId();
|
||||||
|
} else {
|
||||||
|
$currentPageId = (int)$currentPageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($currentPageId === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentPagePropertiesArray = $this->pageRepository->getPage($currentPageId);
|
||||||
|
if ((int)$currentPagePropertiesArray['tx_abtest_variant'] === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$requestedViaCookie = (int)($this->getRequest()->getCookieParams()['ab-' . $currentPageId] ?? '0');
|
||||||
|
$targetPage = $this->getTargetPage($currentPagePropertiesArray, $requestedViaCookie);
|
||||||
|
|
||||||
|
if ($frontendController->id !== (int)$targetPage['uid']) {
|
||||||
|
$frontendController->id = (int)$targetPage['uid'];
|
||||||
|
$frontendController->contentPid = (int)$targetPage['uid'];
|
||||||
|
$frontendController->page = $targetPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
$requestedViaCookie === 0
|
||||||
|
|| (int)$targetPage['uid'] !== $requestedViaCookie
|
||||||
|
) {
|
||||||
|
$this->pageRepository->updateCounter((int)$targetPage['uid'], ++$targetPage['tx_abtest_counter']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->cookie->setRequestedPage($currentPageId);
|
||||||
|
$this->cookie->setActualPage($targetPage['uid']);
|
||||||
|
$this->cookie->setLifetime($targetPage['tx_abtest_cookie_time']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isRequestByBot(): bool
|
||||||
|
{
|
||||||
|
$deviceDetector = new DeviceDetector();
|
||||||
|
$deviceDetector->setUserAgent($_SERVER['HTTP_USER_AGENT']);
|
||||||
|
try {
|
||||||
|
$deviceDetector->parse();
|
||||||
|
return $deviceDetector->isBot();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns 0 if no site could be fetched.
|
||||||
|
*/
|
||||||
|
private function getRootPageId(): int
|
||||||
|
{
|
||||||
|
$site = $this->getRequest()->getAttribute('site');
|
||||||
|
if (!$site instanceof Site) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $site->getRootPageId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getRequest(): ServerRequest
|
||||||
|
{
|
||||||
|
return $GLOBALS['TYPO3_REQUEST'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getTargetPage(array $page, int $cookiePageUid): array
|
||||||
|
{
|
||||||
|
if ($cookiePageUid > 0 && $cookiePageUid === (int)$page['uid']) {
|
||||||
|
return $page;
|
||||||
|
}
|
||||||
|
|
||||||
|
$variantPage = $this->pageRepository->getPage((int)$page['tx_abtest_variant']);
|
||||||
|
|
||||||
|
if (
|
||||||
|
$variantPage !== []
|
||||||
|
&& (
|
||||||
|
($cookiePageUid > 0 && $cookiePageUid === (int)$variantPage['uid'])
|
||||||
|
|| ((int)$variantPage['tx_abtest_counter'] < (int)$page['tx_abtest_counter'])
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return $variantPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $page;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function register(): void
|
||||||
|
{
|
||||||
|
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PostProc'][self::class] = self::class . '->determineContentId';
|
||||||
|
}
|
||||||
|
}
|
51
Classes/TCA/VariantFilter.php
Normal file
51
Classes/TCA/VariantFilter.php
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2023 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WerkraumMedia\ABTest\TCA;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\DataHandling\DataHandler;
|
||||||
|
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
|
||||||
|
|
||||||
|
class VariantFilter
|
||||||
|
{
|
||||||
|
private PageRepository $pageRepository;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
PageRepository $pageRepository
|
||||||
|
) {
|
||||||
|
$this->pageRepository = $pageRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function doFilter(array $parameters, DataHandler $dataHandler): array
|
||||||
|
{
|
||||||
|
return array_filter($parameters['values'], [$this, 'filterPage']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function filterPage(string $pageIdentifier): bool
|
||||||
|
{
|
||||||
|
$uid = (int)str_replace('pages_', '', $pageIdentifier);
|
||||||
|
$page = $this->pageRepository->getPage($uid);
|
||||||
|
$doktype = (int)($page['doktype'] ?? 0);
|
||||||
|
return $doktype === PageRepository::DOKTYPE_DEFAULT;
|
||||||
|
}
|
||||||
|
}
|
15
Configuration/RequestMiddlewares.php
Normal file
15
Configuration/RequestMiddlewares.php
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'frontend' => [
|
||||||
|
'abtest-cookie' => [
|
||||||
|
'target' => \WerkraumMedia\ABTest\Middleware\SetCookie::class,
|
||||||
|
'after' => [
|
||||||
|
'typo3/cms-frontend/prepare-tsfe-rendering',
|
||||||
|
],
|
||||||
|
'before' => [
|
||||||
|
'typo3/cms-frontend/output-compression',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
23
Configuration/Services.php
Normal file
23
Configuration/Services.php
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DanielSiepmann\Configuration;
|
||||||
|
|
||||||
|
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||||
|
use WerkraumMedia\ABTest\Switcher;
|
||||||
|
use WerkraumMedia\ABTest\TCA\VariantFilter;
|
||||||
|
|
||||||
|
return static function (ContainerConfigurator $containerConfigurator) {
|
||||||
|
$services = $containerConfigurator
|
||||||
|
->services()
|
||||||
|
->defaults()
|
||||||
|
->autowire()
|
||||||
|
->autoconfigure()
|
||||||
|
;
|
||||||
|
|
||||||
|
$services->load('WerkraumMedia\\ABTest\\', '../Classes/');
|
||||||
|
|
||||||
|
$services->set(Switcher::class)->public();
|
||||||
|
$services->set(VariantFilter::class)->public();
|
||||||
|
};
|
|
@ -1,61 +1,67 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('pages', [
|
(static function (
|
||||||
'tx_abtest2_b_id' => [
|
string $extensionName = 'abtest',
|
||||||
'exclude' => 1,
|
string $tableName = 'pages'
|
||||||
'label' => 'LLL:EXT:abtest2/Resources/Private/Language/locallang_db.xlf:pages.tx_abtest2_b_id',
|
) {
|
||||||
'config' => [
|
$languagePath = 'LLL:EXT:' . $extensionName . '/Resources/Private/Language/locallang_db.xlf:' . $tableName . '.';
|
||||||
'type' => 'group',
|
|
||||||
'internal_type' => 'db',
|
|
||||||
'allowed' => 'pages',
|
|
||||||
'maxitems' => 1,
|
|
||||||
'size' => 1
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'tx_abtest2_cookie_time' => [
|
|
||||||
'exclude' => 1,
|
|
||||||
'label' => 'LLL:EXT:abtest2/Resources/Private/Language/locallang_db.xlf:pages.tx_abtest2_cookie_time',
|
|
||||||
'config' => [
|
|
||||||
'type' => 'select',
|
|
||||||
'renderType' => 'selectSingle',
|
|
||||||
'items' => [
|
|
||||||
['LLL:EXT:abtest2/Resources/Private/Language/locallang_db.xlf:cookie_1_month', 2419200],
|
|
||||||
['LLL:EXT:abtest2/Resources/Private/Language/locallang_db.xlf:cookie_1_week', 604800],
|
|
||||||
['LLL:EXT:abtest2/Resources/Private/Language/locallang_db.xlf:cookie_1_day', 86400],
|
|
||||||
['LLL:EXT:abtest2/Resources/Private/Language/locallang_db.xlf:cookie_12_day', 43200],
|
|
||||||
['LLL:EXT:abtest2/Resources/Private/Language/locallang_db.xlf:cookie_1_hour', 3600],
|
|
||||||
['LLL:EXT:abtest2/Resources/Private/Language/locallang_db.xlf:cookie_1_minute', 60]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'tx_abtest2_header' => [
|
|
||||||
'exclude' => 1,
|
|
||||||
'label' => 'LLL:EXT:abtest2/Resources/Private/Language/locallang_db.xlf:pages.tx_abtest2_header',
|
|
||||||
'config' => [
|
|
||||||
'type' => 'text'
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'tx_abtest2_footer' => [
|
|
||||||
'exclude' => 1,
|
|
||||||
'label' => 'LLL:EXT:abtest2/Resources/Private/Language/locallang_db.xlf:pages.tx_abtest2_footer',
|
|
||||||
'config' => [
|
|
||||||
'type' => 'text'
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'tx_abtest2_counter' => [
|
|
||||||
'exclude' => 1,
|
|
||||||
'label' => 'LLL:EXT:abtest2/Resources/Private/Language/locallang_db.xlf:pages.tx_abtest2_counter',
|
|
||||||
'config' => [
|
|
||||||
'type' => 'input'
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
|
|
||||||
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes('pages',
|
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns($tableName, [
|
||||||
'--palette--;LLL:EXT:abtest2/Resources/Private/Language/locallang_db.xlf:palette_title;tx_abtest2', '',
|
'tx_abtest_variant' => [
|
||||||
'after:subtitle');
|
'exclude' => 1,
|
||||||
|
'label' => $languagePath . 'tx_abtest_variant',
|
||||||
$GLOBALS['TCA']['pages']['palettes']['tx_abtest2'] = array(
|
'config' => [
|
||||||
'showitem' => 'tx_abtest2_b_id,--linebreak--,tx_abtest2_header,tx_abtest2_footer,tx_abtest2_cookie_time,tx_abtest2_counter'
|
'type' => 'group',
|
||||||
);
|
'allowed' => 'pages',
|
||||||
|
'maxitems' => 1,
|
||||||
|
'minitems' => 0,
|
||||||
|
'size' => 1,
|
||||||
|
'suggestOptions' => [
|
||||||
|
'default' => [
|
||||||
|
'addWhere' => 'AND pages.doktype = ' . \TYPO3\CMS\Core\Domain\Repository\PageRepository::DOKTYPE_DEFAULT,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'filter' => [
|
||||||
|
[
|
||||||
|
'userFunc' => \WerkraumMedia\ABTest\TCA\VariantFilter::class . '->doFilter',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'tx_abtest_cookie_time' => [
|
||||||
|
'exclude' => 1,
|
||||||
|
'label' => $languagePath . 'tx_abtest_cookie_time',
|
||||||
|
'config' => [
|
||||||
|
'type' => 'input',
|
||||||
|
'eval' => 'int',
|
||||||
|
'size' => 10,
|
||||||
|
'valuePicker' => [
|
||||||
|
'items' => [
|
||||||
|
[$languagePath . 'tx_abtest_cookie_time.cookie_1_month', 2419200],
|
||||||
|
[$languagePath . 'tx_abtest_cookie_time.cookie_1_week', 604800],
|
||||||
|
[$languagePath . 'tx_abtest_cookie_time.cookie_1_day', 86400],
|
||||||
|
[$languagePath . 'tx_abtest_cookie_time.cookie_12_days', 43200],
|
||||||
|
[$languagePath . 'tx_abtest_cookie_time.cookie_1_hour', 3600],
|
||||||
|
[$languagePath . 'tx_abtest_cookie_time.cookie_1_minute', 60],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'tx_abtest_counter' => [
|
||||||
|
'exclude' => 1,
|
||||||
|
'label' => $languagePath . 'tx_abtest_counter',
|
||||||
|
'config' => [
|
||||||
|
'type' => 'input',
|
||||||
|
'eval' => 'int',
|
||||||
|
'size' => 10,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes(
|
||||||
|
$tableName,
|
||||||
|
'--div--;' . $languagePath . 'div_title,tx_abtest_variant,tx_abtest_cookie_time,tx_abtest_counter',
|
||||||
|
'',
|
||||||
|
'after:content_from_pid'
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,16 +0,0 @@
|
||||||
.. ==================================================
|
|
||||||
.. FOR YOUR INFORMATION
|
|
||||||
.. --------------------------------------------------
|
|
||||||
.. -*- coding: utf-8 -*- with BOM.
|
|
||||||
|
|
||||||
.. include:: ../../Includes.txt
|
|
||||||
|
|
||||||
|
|
||||||
Best practice
|
|
||||||
-------------
|
|
||||||
|
|
||||||
.. only:: html
|
|
||||||
|
|
||||||
This chapter describes some best practice concepts
|
|
||||||
|
|
||||||
TODO
|
|
|
@ -1,23 +0,0 @@
|
||||||
.. ==================================================
|
|
||||||
.. FOR YOUR INFORMATION
|
|
||||||
.. --------------------------------------------------
|
|
||||||
.. -*- coding: utf-8 -*- with BOM.
|
|
||||||
|
|
||||||
.. include:: ../Includes.txt
|
|
||||||
|
|
||||||
|
|
||||||
.. _admin-manual:
|
|
||||||
|
|
||||||
For administrators
|
|
||||||
==================
|
|
||||||
|
|
||||||
.. only:: html
|
|
||||||
|
|
||||||
This chapter describes how to manage the extension from a superuser point of view.
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 5
|
|
||||||
:titlesonly:
|
|
||||||
|
|
||||||
Installation/Index
|
|
||||||
BestPractice/Index
|
|
|
@ -1,42 +0,0 @@
|
||||||
.. ==================================================
|
|
||||||
.. FOR YOUR INFORMATION
|
|
||||||
.. --------------------------------------------------
|
|
||||||
.. -*- coding: utf-8 -*- with BOM.
|
|
||||||
|
|
||||||
.. include:: ../../Includes.txt
|
|
||||||
|
|
||||||
.. _installation:
|
|
||||||
|
|
||||||
Installation
|
|
||||||
============
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
The extension needs to be installed as any other extension of TYPO3 CMS:
|
|
||||||
|
|
||||||
#. Switch to the module “Extension Manager”.
|
|
||||||
|
|
||||||
#. Get the extension
|
|
||||||
|
|
||||||
#. **Get it from the Extension Manager:** Press the “Retrieve/Update”
|
|
||||||
button and search for the extension key *abtest2* and import the
|
|
||||||
extension from the repository.
|
|
||||||
|
|
||||||
#. **Get it from typo3.org:** You can always get current version from
|
|
||||||
`http://typo3.org/extensions/repository/view/abtest2/current/
|
|
||||||
<http://typo3.org/extensions/repository/view/abtest2/current/>`_ by
|
|
||||||
downloading the zip version. Upload
|
|
||||||
the file afterwards in the Extension Manager.
|
|
||||||
|
|
||||||
#. **Use composer**: Use `composer require svewap/abtest2`.
|
|
||||||
|
|
||||||
#. The Extension Manager offers some basic configuration which is
|
|
||||||
explained :ref:`here <extensionManager>`.
|
|
||||||
|
|
||||||
Latest version from git
|
|
||||||
-----------------------
|
|
||||||
You can get the latest version from git by using the git command:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
git clone https://github.com/svewap/abtest2.git
|
|
Binary file not shown.
Before Width: | Height: | Size: 131 KiB |
Binary file not shown.
Before Width: | Height: | Size: 21 KiB |
|
@ -1,29 +0,0 @@
|
||||||
.. ==================================================
|
|
||||||
.. FOR YOUR INFORMATION
|
|
||||||
.. --------------------------------------------------
|
|
||||||
.. -*- coding: utf-8 -*- with BOM.
|
|
||||||
|
|
||||||
.. This is 'Includes.txt'. It is included at the very top of each and
|
|
||||||
every ReST source file in this documentation project (= manual).
|
|
||||||
|
|
||||||
|
|
||||||
.. ==================================================
|
|
||||||
.. DEFINE SOME TEXT ROLES
|
|
||||||
.. --------------------------------------------------
|
|
||||||
|
|
||||||
.. role:: typoscript(code)
|
|
||||||
|
|
||||||
.. role:: ts(typoscript)
|
|
||||||
:class: typoscript
|
|
||||||
|
|
||||||
.. role:: php(code)
|
|
||||||
|
|
||||||
.. highlight:: php
|
|
||||||
|
|
||||||
.. |img-demo| image:: /Images/demo.gif
|
|
||||||
.. :border: 0
|
|
||||||
.. :align: left
|
|
||||||
|
|
||||||
.. |img-page-settings| image:: /Images/page_settings.png
|
|
||||||
.. :border: 0
|
|
||||||
.. :align: left
|
|
|
@ -1,54 +0,0 @@
|
||||||
.. ==================================================
|
|
||||||
.. FOR YOUR INFORMATION
|
|
||||||
.. --------------------------------------------------
|
|
||||||
.. -*- coding: utf-8 -*- with BOM.
|
|
||||||
|
|
||||||
.. include:: Includes.txt
|
|
||||||
|
|
||||||
|
|
||||||
.. _start:
|
|
||||||
|
|
||||||
=============================================================
|
|
||||||
TYPO3 extension for A/B tests
|
|
||||||
=============================================================
|
|
||||||
|
|
||||||
.. only:: html
|
|
||||||
|
|
||||||
:Classification:
|
|
||||||
abtest2
|
|
||||||
|
|
||||||
:Version:
|
|
||||||
|release|
|
|
||||||
|
|
||||||
:Language:
|
|
||||||
en
|
|
||||||
|
|
||||||
:Keywords:
|
|
||||||
abtest
|
|
||||||
|
|
||||||
:Copyright:
|
|
||||||
2017
|
|
||||||
|
|
||||||
:Author:
|
|
||||||
Sven Wappler
|
|
||||||
|
|
||||||
:License:
|
|
||||||
This document is published under the Open Content License
|
|
||||||
available from http://www.opencontent.org/opl.shtml
|
|
||||||
|
|
||||||
:Rendered:
|
|
||||||
|today|
|
|
||||||
|
|
||||||
The content of this document is related to TYPO3,
|
|
||||||
a GNU/GPL CMS/Framework available from `www.typo3.org <http://www.typo3.org/>`_.
|
|
||||||
|
|
||||||
|
|
||||||
**Table of Contents**
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 3
|
|
||||||
:titlesonly:
|
|
||||||
:glob:
|
|
||||||
|
|
||||||
Introduction/Index
|
|
||||||
AdministratorManual/Index
|
|
|
@ -1,23 +0,0 @@
|
||||||
.. ==================================================
|
|
||||||
.. FOR YOUR INFORMATION
|
|
||||||
.. --------------------------------------------------
|
|
||||||
.. -*- coding: utf-8 -*- with BOM.
|
|
||||||
|
|
||||||
.. include:: ../../Includes.txt
|
|
||||||
|
|
||||||
.. _about:
|
|
||||||
|
|
||||||
What does it do?
|
|
||||||
================
|
|
||||||
|
|
||||||
This extension supports TYPO3 administrators in performing A/B tests. This is useful when a site owner want to measure whether a new version improves or reduces user interaction compared to the current version.
|
|
||||||
|
|
||||||
|
|
||||||
**Features of the extension**
|
|
||||||
|
|
||||||
- Caching of each page version
|
|
||||||
- A real 50/50% chance. That means: No selection by random, because of the unreliable random method. So the versions are always taken alternately.
|
|
||||||
- Complete different content with same page id. So only one URL for two versions. The displayed version is determined by the cookie value.
|
|
||||||
|
|
||||||
|
|
||||||
|img-demo|
|
|
|
@ -1,21 +0,0 @@
|
||||||
.. ==================================================
|
|
||||||
.. FOR YOUR INFORMATION
|
|
||||||
.. --------------------------------------------------
|
|
||||||
.. -*- coding: utf-8 -*- with BOM.
|
|
||||||
|
|
||||||
.. _introduction:
|
|
||||||
|
|
||||||
Introduction
|
|
||||||
============
|
|
||||||
|
|
||||||
.. only:: html
|
|
||||||
|
|
||||||
This chapter gives you a basic introduction about the TYPO3 CMS extension "*abtest2*".
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 5
|
|
||||||
:titlesonly:
|
|
||||||
|
|
||||||
About/Index
|
|
||||||
Support/Index
|
|
||||||
Thanks/Index
|
|
|
@ -1,26 +0,0 @@
|
||||||
.. ==================================================
|
|
||||||
.. FOR YOUR INFORMATION
|
|
||||||
.. --------------------------------------------------
|
|
||||||
.. -*- coding: utf-8 -*- with BOM.
|
|
||||||
|
|
||||||
.. include:: ../../Includes.txt
|
|
||||||
|
|
||||||
.. _support:
|
|
||||||
|
|
||||||
Need Support?
|
|
||||||
=============
|
|
||||||
There are various ways to get support for this extension!
|
|
||||||
|
|
||||||
Stackoverflow
|
|
||||||
-------------
|
|
||||||
Please use https://stackoverflow.com to get best support. Tags you should use are `typo3` and `abtest2`.
|
|
||||||
|
|
||||||
Sponsoring
|
|
||||||
----------
|
|
||||||
If you need a feature which is not yet implemented, feel free to contact me anytime!
|
|
||||||
|
|
||||||
Private/Personal support
|
|
||||||
------------------------
|
|
||||||
If you need private or personal support, ask one of the developers for it.
|
|
||||||
|
|
||||||
**Be aware that this support might not be free!**
|
|
|
@ -1,32 +0,0 @@
|
||||||
.. ==================================================
|
|
||||||
.. FOR YOUR INFORMATION
|
|
||||||
.. --------------------------------------------------
|
|
||||||
.. -*- coding: utf-8 -*- with BOM.
|
|
||||||
|
|
||||||
.. include:: ../../Includes.txt
|
|
||||||
|
|
||||||
.. _thanks:
|
|
||||||
|
|
||||||
Say thanks!
|
|
||||||
===========
|
|
||||||
This extension and manual has been created in hours, mostly by a single person.
|
|
||||||
It is actively maintained to fit all supported TYPO3 versions, user interface concepts and best practice approaches.
|
|
||||||
|
|
||||||
If this extension helps you in anyway to achieve your requirements, please think about giving something back.
|
|
||||||
Or you want to sponsor a feature to extend something that's already there? Then find some ideas to make me happy below - I'm looking forward to get in contact with you!
|
|
||||||
|
|
||||||
Nice mails
|
|
||||||
^^^^^^^^^^
|
|
||||||
Some nice words fit every time - just drop me some kind words by mail to make me happy :-)
|
|
||||||
|
|
||||||
Talk about it
|
|
||||||
^^^^^^^^^^^^^
|
|
||||||
If you like what I've built, then share it wherever it fits. Maybe this will help to spread the word and find others that can profit from it, too.
|
|
||||||
|
|
||||||
Let's have a tee
|
|
||||||
^^^^^^^^^^^^^^^^^^^
|
|
||||||
If you're in the region of Aachen, just let me know. I'm always looking forward to meeting new and known faces and to exchange on several topics.
|
|
||||||
|
|
||||||
Money
|
|
||||||
^^^^^
|
|
||||||
If you have too much of it and want to share parts of your money with me, just let me know and we'll find a way to organize that :-)
|
|
|
@ -1,48 +0,0 @@
|
||||||
[html_theme_options]
|
|
||||||
project_contact =
|
|
||||||
use_opensearch =
|
|
||||||
project_home =
|
|
||||||
project_issues =
|
|
||||||
github_revision_msg =
|
|
||||||
github_branch =
|
|
||||||
github_repository =
|
|
||||||
project_repository =
|
|
||||||
project_discussions =
|
|
||||||
github_sphinx_locale =
|
|
||||||
github_commit_hash =
|
|
||||||
|
|
||||||
[intersphinx_mapping]
|
|
||||||
t3tsref = http://docs.typo3.org/typo3cms/TyposcriptReference/
|
|
||||||
t3editors = http://docs.typo3.org/typo3cms/EditorsTutorial/
|
|
||||||
t3start = http://docs.typo3.org/typo3cms/GettingStartedTutorial/
|
|
||||||
|
|
||||||
[latex_elements]
|
|
||||||
papersize = a4paper
|
|
||||||
preamble = \usepackage{typo3}
|
|
||||||
pointsize = 10pt
|
|
||||||
|
|
||||||
[general]
|
|
||||||
project = abtest2
|
|
||||||
release = 1.0.0
|
|
||||||
version = 1.0
|
|
||||||
copyright = 2017
|
|
||||||
|
|
||||||
[notify]
|
|
||||||
about_new_build = no
|
|
||||||
# have one or more receivers notified
|
|
||||||
# about_new_build = email-1 [, email2, ...]
|
|
||||||
|
|
||||||
|
|
||||||
# About Settings.cfg
|
|
||||||
|
|
||||||
# normal:
|
|
||||||
# https://github.com/marble/typo3-docs-typo3-org-resources/blob/master/TemplatesForCopying/ExampleFiles/Settings-minimal.cfg
|
|
||||||
|
|
||||||
# extensive:
|
|
||||||
# https://github.com/marble/typo3-docs-typo3-org-resources/blob/master/TemplatesForCopying/ExampleFiles/Settings-extensive.cfg
|
|
||||||
|
|
||||||
# Example files:
|
|
||||||
# https://github.com/marble/typo3-docs-typo3-org-resources/tree/master/TemplatesForCopying/ExampleFiles
|
|
||||||
|
|
||||||
# More:
|
|
||||||
# http://mbless.de/blog/2015/10/24/a-new-task-for-an-old-server.html#ini-files
|
|
|
@ -1,30 +0,0 @@
|
||||||
# This is the project specific Settings.yml file.
|
|
||||||
# Place Sphinx specific build information here.
|
|
||||||
# Settings given here will replace the settings of 'conf.py'.
|
|
||||||
|
|
||||||
---
|
|
||||||
conf.py:
|
|
||||||
copyright: 2017
|
|
||||||
project: abtest2
|
|
||||||
version: 1.0
|
|
||||||
release: 1.0.0
|
|
||||||
latex_documents:
|
|
||||||
- - Index
|
|
||||||
html_theme_options:
|
|
||||||
github_repository: svewap/abtest2
|
|
||||||
github_branch: master
|
|
||||||
latex_elements:
|
|
||||||
papersize: a4paper
|
|
||||||
pointsize: 10pt
|
|
||||||
preamble: \usepackage{typo3}
|
|
||||||
intersphinx_mapping:
|
|
||||||
t3tsref:
|
|
||||||
- http://docs.typo3.org/typo3cms/TyposcriptReference/
|
|
||||||
- null
|
|
||||||
t3start:
|
|
||||||
- http://docs.typo3.org/typo3cms/GettingStartedTutorial/
|
|
||||||
- null
|
|
||||||
t3editors:
|
|
||||||
- http://docs.typo3.org/typo3cms/EditorsTutorial/
|
|
||||||
- null
|
|
||||||
...
|
|
59
README.md
59
README.md
|
@ -1,4 +1,4 @@
|
||||||
# abtest2 TYPO3 Extension
|
# abtest TYPO3 Extension
|
||||||
|
|
||||||
Extension for A/B-Tests
|
Extension for A/B-Tests
|
||||||
|
|
||||||
|
@ -19,60 +19,5 @@ Additional header information may be specified both for the original version as
|
||||||
|
|
||||||
#### Demo
|
#### Demo
|
||||||
|
|
||||||
![Demo](https://raw.githubusercontent.com/svewap/abtest2/master/Documentation/Images/demo.gif)
|
![Demo](https://raw.githubusercontent.com/werkraum-media/abtest/master/Documentation/Images/demo.gif)
|
||||||
|
|
||||||
#### Example for Google Tag Manager:
|
|
||||||
|
|
||||||
You have two options to define the parameter: By page settings or by TypoScript:
|
|
||||||
|
|
||||||
##### Additional Header Information at page settings
|
|
||||||
|
|
||||||
On original page (version A):
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
<script>
|
|
||||||
dataLayer.push({'variant': 'a'});
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
On version B:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
<script>
|
|
||||||
dataLayer.push({'variant': 'b'});
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
##### TypoScript
|
|
||||||
|
|
||||||
```typo3_typoscript
|
|
||||||
[globalVar = GP:abtest = a]
|
|
||||||
page.headerData.129 = TEXT
|
|
||||||
page.headerData.129.value (
|
|
||||||
<script>
|
|
||||||
dataLayer = [{
|
|
||||||
'variant': 'a'
|
|
||||||
}];
|
|
||||||
</script>
|
|
||||||
)
|
|
||||||
[global]
|
|
||||||
[globalVar = GP:abtest = b]
|
|
||||||
page.headerData.129 = TEXT
|
|
||||||
page.headerData.129.value (
|
|
||||||
<script>
|
|
||||||
dataLayer = [{
|
|
||||||
'variant': 'b'
|
|
||||||
}];
|
|
||||||
</script>
|
|
||||||
)
|
|
||||||
[global]
|
|
||||||
|
|
||||||
page.headerData.130 = TEXT
|
|
||||||
page.headerData.130.value (
|
|
||||||
<!-- Google Tag Manager -->
|
|
||||||
....
|
|
||||||
<!-- End Google Tag Manager -->
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
|
@ -1,57 +1,48 @@
|
||||||
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<xliff version="1.0">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file source-language="en" target-language="de" datatype="plaintext" original="messages" date="2017-10-20T11:36:00Z"
|
<file target-language="de" source-language="en" datatype="plaintext" original="messages" date="2023-03-02T09:26:47Z" product-name="abtest">
|
||||||
product-name="abtest2">
|
<header/>
|
||||||
<header/>
|
<body>
|
||||||
<body>
|
<trans-unit id="pages.div_title" approved="yes">
|
||||||
<trans-unit id="pages.tx_abtest2_b_id">
|
<source>A/B Testing</source>
|
||||||
<source>Site B</source>
|
<target>A/B Testing</target>
|
||||||
<target>Seite B</target>
|
</trans-unit>
|
||||||
</trans-unit>
|
<trans-unit id="pages.tx_abtest_variant" approved="yes">
|
||||||
<trans-unit id="pages.tx_abtest2_cookie_time">
|
<source>Variant</source>
|
||||||
<source>Cookie Lifetime</source>
|
<target>Variante</target>
|
||||||
<target>Cookie Lebenszeit</target>
|
</trans-unit>
|
||||||
</trans-unit>
|
<trans-unit id="pages.tx_abtest_cookie_time" approved="yes">
|
||||||
<trans-unit id="pages.tx_abtest2_header">
|
<source>Cookie Lifetime</source>
|
||||||
<source>Additional Header Information</source>
|
<target>Cookie Lebenszeit</target>
|
||||||
<target>Zusätzliche Header Informationen</target>
|
</trans-unit>
|
||||||
</trans-unit>
|
<trans-unit id="pages.tx_abtest_cookie_time.cookie_1_month" approved="yes">
|
||||||
<trans-unit id="pages.tx_abtest2_footer">
|
<source>1 month</source>
|
||||||
<source>Additional footer information</source>
|
<target>1 Monat</target>
|
||||||
<target>Zusätzliche Footer Informationen</target>
|
</trans-unit>
|
||||||
</trans-unit>
|
<trans-unit id="pages.tx_abtest_cookie_time.cookie_1_week" approved="yes">
|
||||||
<trans-unit id="pages.tx_abtest2_counter">
|
<source>1 week</source>
|
||||||
<source>Counter</source>
|
<target>1 Woche</target>
|
||||||
<target>Zähler</target>
|
</trans-unit>
|
||||||
</trans-unit>
|
<trans-unit id="pages.tx_abtest_cookie_time.cookie_1_day" approved="yes">
|
||||||
<trans-unit id="palette_title">
|
<source>1 day</source>
|
||||||
<source>AB Test Settings</source>
|
<target>1 Tag</target>
|
||||||
<target>AB Test Einstellungen</target>
|
</trans-unit>
|
||||||
</trans-unit>
|
<trans-unit id="pages.tx_abtest_cookie_time.cookie_12_days" approved="yes">
|
||||||
<trans-unit id="cookie_1_month">
|
<source>1/2 day</source>
|
||||||
<source>1 month</source>
|
<target>1/2 Tag</target>
|
||||||
<target>1 Monat</target>
|
</trans-unit>
|
||||||
</trans-unit>
|
<trans-unit id="pages.tx_abtest_cookie_time.cookie_1_hour" approved="yes">
|
||||||
<trans-unit id="cookie_1_week">
|
<source>1 hour</source>
|
||||||
<source>1 week</source>
|
<target>1 Stunde</target>
|
||||||
<target>1 Woche</target>
|
</trans-unit>
|
||||||
</trans-unit>
|
<trans-unit id="pages.tx_abtest_cookie_time.cookie_1_minute" approved="yes">
|
||||||
<trans-unit id="cookie_1_day">
|
<source>1 minute</source>
|
||||||
<source>1 day</source>
|
<target>1 Minute</target>
|
||||||
<target>1 Tag</target>
|
</trans-unit>
|
||||||
</trans-unit>
|
<trans-unit id="pages.tx_abtest_counter" approved="yes">
|
||||||
<trans-unit id="cookie_12_day">
|
<source>Counter</source>
|
||||||
<source>1/2 day</source>
|
<target>Zähler</target>
|
||||||
<target>1/2 Tag</target>
|
</trans-unit>
|
||||||
</trans-unit>
|
</body>
|
||||||
<trans-unit id="cookie_1_hour">
|
</file>
|
||||||
<source>1 hour</source>
|
</xliff>
|
||||||
<target>1 Stunde</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="cookie_1_minute">
|
|
||||||
<source>1 minute</source>
|
|
||||||
<target>1 Minute</target>
|
|
||||||
</trans-unit>
|
|
||||||
</body>
|
|
||||||
</file>
|
|
||||||
</xliff>
|
|
||||||
|
|
|
@ -1,45 +1,38 @@
|
||||||
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<xliff version="1.0">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file source-language="en" datatype="plaintext" original="messages" date="2017-10-20T11:36:00Z"
|
<file source-language="en" datatype="plaintext" original="messages" date="2023-03-02T09:19:28Z" product-name="abtest">
|
||||||
product-name="abtest2">
|
<header/>
|
||||||
<header/>
|
<body>
|
||||||
<body>
|
<trans-unit id="pages.div_title" approved="yes">
|
||||||
<trans-unit id="pages.tx_abtest2_b_id">
|
<source>A/B Testing</source>
|
||||||
<source>Site B</source>
|
</trans-unit>
|
||||||
</trans-unit>
|
<trans-unit id="pages.tx_abtest_variant" approved="yes">
|
||||||
<trans-unit id="pages.tx_abtest2_cookie_time">
|
<source>Variant</source>
|
||||||
<source>Cookie Lifetime</source>
|
</trans-unit>
|
||||||
</trans-unit>
|
<trans-unit id="pages.tx_abtest_cookie_time" approved="yes">
|
||||||
<trans-unit id="pages.tx_abtest2_header">
|
<source>Cookie Lifetime</source>
|
||||||
<source>Additional header information</source>
|
</trans-unit>
|
||||||
</trans-unit>
|
<trans-unit id="pages.tx_abtest_cookie_time.cookie_1_month" approved="yes">
|
||||||
<trans-unit id="pages.tx_abtest2_footer">
|
<source>1 month</source>
|
||||||
<source>Additional footer information</source>
|
</trans-unit>
|
||||||
</trans-unit>
|
<trans-unit id="pages.tx_abtest_cookie_time.cookie_1_week" approved="yes">
|
||||||
<trans-unit id="pages.tx_abtest2_counter">
|
<source>1 week</source>
|
||||||
<source>Counter</source>
|
</trans-unit>
|
||||||
</trans-unit>
|
<trans-unit id="pages.tx_abtest_cookie_time.cookie_1_day" approved="yes">
|
||||||
<trans-unit id="palette_title">
|
<source>1 day</source>
|
||||||
<source>AB Test Settings</source>
|
</trans-unit>
|
||||||
</trans-unit>
|
<trans-unit id="pages.tx_abtest_cookie_time.cookie_12_days" approved="yes">
|
||||||
<trans-unit id="cookie_1_month">
|
<source>1/2 day</source>
|
||||||
<source>1 month</source>
|
</trans-unit>
|
||||||
</trans-unit>
|
<trans-unit id="pages.tx_abtest_cookie_time.cookie_1_hour" approved="yes">
|
||||||
<trans-unit id="cookie_1_week">
|
<source>1 hour</source>
|
||||||
<source>1 week</source>
|
</trans-unit>
|
||||||
</trans-unit>
|
<trans-unit id="pages.tx_abtest_cookie_time.cookie_1_minute" approved="yes">
|
||||||
<trans-unit id="cookie_1_day">
|
<source>1 minute</source>
|
||||||
<source>1 day</source>
|
</trans-unit>
|
||||||
</trans-unit>
|
<trans-unit id="pages.tx_abtest_counter" approved="yes">
|
||||||
<trans-unit id="cookie_12_day">
|
<source>Counter</source>
|
||||||
<source>1/2 day</source>
|
</trans-unit>
|
||||||
</trans-unit>
|
</body>
|
||||||
<trans-unit id="cookie_1_hour">
|
</file>
|
||||||
<source>1 hour</source>
|
</xliff>
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="cookie_1_minute">
|
|
||||||
<source>1 minute</source>
|
|
||||||
</trans-unit>
|
|
||||||
</body>
|
|
||||||
</file>
|
|
||||||
</xliff>
|
|
||||||
|
|
10
Tests/Fixtures/BasicDatabase.csv
Normal file
10
Tests/Fixtures/BasicDatabase.csv
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
"pages",,,,,,,,,,,,,,
|
||||||
|
,"uid","pid","slug","title",tx_abtest_variant,hidden,tx_abtest_cookie_time,,,,,,,
|
||||||
|
,1,0,"/","Page 1 Title (No Variant)",0,0,604800,,,,,,,
|
||||||
|
,2,1,"/page-2","Page 2 Title (Variant A)",3,0,604800,,,,,,,
|
||||||
|
,3,1,"/page-3","Page 3 Title (Variant B)",0,0,604800,,,,,,,
|
||||||
|
,4,1,"/page-4","Page 4 Title (Variant A)",5,0,2419200,,,,,,,
|
||||||
|
,5,1,"/page-5","Page 5 Title (Variant B)",0,1,604800,,,,,,,
|
||||||
|
"sys_template",,,,,,,,,,,,,,
|
||||||
|
,"uid","pid","root","clear","constants","config",,,,,,,,
|
||||||
|
,1,1,1,3,"databasePlatform = mysql","<INCLUDE_TYPOSCRIPT: source=""FILE:EXT:abtest/Tests/Fixtures/FrontendRendering.typoscript"">",,,,,,,,
|
|
16
Tests/Fixtures/FrontendRendering.typoscript
Normal file
16
Tests/Fixtures/FrontendRendering.typoscript
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
config {
|
||||||
|
debug = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
page = PAGE
|
||||||
|
page {
|
||||||
|
config {
|
||||||
|
debug = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
5 = TEXT
|
||||||
|
5.field = title
|
||||||
|
|
||||||
|
10 = TEXT
|
||||||
|
10.value = Example content from TypoScript
|
||||||
|
}
|
32
Tests/Fixtures/Sites/default/config.yaml
Normal file
32
Tests/Fixtures/Sites/default/config.yaml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
base: /
|
||||||
|
languages:
|
||||||
|
-
|
||||||
|
title: English
|
||||||
|
enabled: true
|
||||||
|
base: /
|
||||||
|
typo3Language: default
|
||||||
|
locale: en_GB.UTF-8
|
||||||
|
iso-639-1: en
|
||||||
|
websiteTitle: ''
|
||||||
|
navigationTitle: English
|
||||||
|
hreflang: en-GB
|
||||||
|
direction: ''
|
||||||
|
flag: gb
|
||||||
|
languageId: 0
|
||||||
|
fallbackType: strict
|
||||||
|
fallbacks: '0'
|
||||||
|
-
|
||||||
|
title: Deutsch
|
||||||
|
enabled: true
|
||||||
|
base: /de
|
||||||
|
typo3Language: de
|
||||||
|
locale: de_DE.UTF-8
|
||||||
|
iso-639-1: de
|
||||||
|
navigationTitle: Deutsch
|
||||||
|
hreflang: de-DE
|
||||||
|
direction: ''
|
||||||
|
flag: de
|
||||||
|
websiteTitle: ''
|
||||||
|
languageId: 1
|
||||||
|
rootPageId: 1
|
||||||
|
websiteTitle: 'Example Website'
|
309
Tests/Functional/FrontendRenderingTest.php
Normal file
309
Tests/Functional/FrontendRenderingTest.php
Normal file
|
@ -0,0 +1,309 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2023 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WerkraumMedia\ABTest\Tests\Functional;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Cookie;
|
||||||
|
use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
|
||||||
|
use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalResponse;
|
||||||
|
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
|
||||||
|
|
||||||
|
class FrontendRenderingTest extends FunctionalTestCase
|
||||||
|
{
|
||||||
|
protected $testExtensionsToLoad = [
|
||||||
|
'typo3conf/ext/abtest',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $pathsToLinkInTestInstance = [
|
||||||
|
'typo3conf/ext/abtest/Tests/Fixtures/Sites' => 'typo3conf/sites',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->setUpBackendUserFromFixture(1);
|
||||||
|
|
||||||
|
$this->importCSVDataSet(__DIR__ . '/../Fixtures/BasicDatabase.csv');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function opensDefaultPageIfNothingIsConfigured(): void
|
||||||
|
{
|
||||||
|
$request = new InternalRequest();
|
||||||
|
$request = $request->withPageId(1);
|
||||||
|
$result = $this->executeFrontendRequest($request);
|
||||||
|
|
||||||
|
self::assertSame(200, $result->getStatusCode());
|
||||||
|
self::assertSame('', $result->getHeaderLine('Set-Cookie'));
|
||||||
|
self::assertStringContainsString('Page 1 Title (No Variant)', $result->getBody()->__toString());
|
||||||
|
$this->assertPageIsNotCached($result);
|
||||||
|
$this->assertCounterOfPage(1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function opensVariantAForFirstVisitor(): void
|
||||||
|
{
|
||||||
|
$request = new InternalRequest();
|
||||||
|
$request = $request->withPageId(2);
|
||||||
|
$result = $this->executeFrontendRequest($request);
|
||||||
|
|
||||||
|
self::assertSame(200, $result->getStatusCode());
|
||||||
|
self::assertStringContainsString('Page 2 Title (Variant A)', $result->getBody()->__toString());
|
||||||
|
$this->assertPageIsNotCached($result);
|
||||||
|
$this->assertCookie($result, 'ab-2', '2');
|
||||||
|
$this->assertCounterOfPage(2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function opensVariantBForSecondVisitor(): void
|
||||||
|
{
|
||||||
|
$this->opensVariantAForFirstVisitor();
|
||||||
|
|
||||||
|
$request = new InternalRequest();
|
||||||
|
$request = $request->withPageId(2);
|
||||||
|
$result = $this->executeFrontendRequest($request);
|
||||||
|
|
||||||
|
self::assertSame(200, $result->getStatusCode());
|
||||||
|
self::assertStringContainsString('Page 3 Title (Variant B)', $result->getBody()->__toString());
|
||||||
|
$this->assertCookie($result, 'ab-2', '3');
|
||||||
|
$this->assertPageIsNotCached($result);
|
||||||
|
$this->assertCounterOfPage(2, 1);
|
||||||
|
$this->assertCounterOfPage(3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function opensVariantStoredInCookie(): void
|
||||||
|
{
|
||||||
|
$this->opensVariantAForFirstVisitor();
|
||||||
|
|
||||||
|
$request = new InternalRequest();
|
||||||
|
$request = $request->withPageId(2);
|
||||||
|
$request = $request->withAddedHeader('Cookie', 'ab-2=2');
|
||||||
|
$result = $this->executeFrontendRequest($request);
|
||||||
|
|
||||||
|
self::assertSame(200, $result->getStatusCode());
|
||||||
|
self::assertStringContainsString('Page 2 Title (Variant A)', $result->getBody()->__toString());
|
||||||
|
$this->assertPageIsCached($result);
|
||||||
|
$this->assertCookie($result, 'ab-2', '2');
|
||||||
|
// 1 from first visit, but not 2 as 2nd visit is via cookie.
|
||||||
|
$this->assertCounterOfPage(2, 1, 'Opening from cookie should not increase counter.');
|
||||||
|
$this->assertCounterOfPage(3, 0, 'Opening from cookie should not increase counter.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function opensDefaultPageIfBotWasDetected(): void
|
||||||
|
{
|
||||||
|
$request = new InternalRequest();
|
||||||
|
$request = $request->withPageId(2);
|
||||||
|
$request = $request->withAddedHeader('User-Agent', 'Storebot-Google');
|
||||||
|
$result = $this->executeFrontendRequest($request);
|
||||||
|
|
||||||
|
self::assertSame(200, $result->getStatusCode());
|
||||||
|
self::assertStringContainsString('Page 2 Title (Variant A)', $result->getBody()->__toString());
|
||||||
|
$this->assertPageIsNotCached($result);
|
||||||
|
$this->assertCookieWasNotSet($result);
|
||||||
|
$this->assertCounterOfPage(2, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function opensRequestedPageIfVariantPageDoesNotExist(): void
|
||||||
|
{
|
||||||
|
$request = new InternalRequest();
|
||||||
|
$request = $request->withPageId(4);
|
||||||
|
$request = $request->withAddedHeader('Cookie', 'ab-4=5');
|
||||||
|
$result = $this->executeFrontendRequest($request);
|
||||||
|
|
||||||
|
self::assertSame(200, $result->getStatusCode());
|
||||||
|
self::assertStringContainsString('Page 4 Title (Variant A)', $result->getBody()->__toString());
|
||||||
|
$this->assertPageIsNotCached($result);
|
||||||
|
$this->assertCookie($result, 'ab-4', '4');
|
||||||
|
$this->assertCounterOfPage(4, 1);
|
||||||
|
$this->assertCounterOfPage(5, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function opensRequestedPageIfCookieDoesNotMatchRequestedPage(): void
|
||||||
|
{
|
||||||
|
$request = new InternalRequest();
|
||||||
|
$request = $request->withPageId(2);
|
||||||
|
$request = $request->withAddedHeader('Cookie', 'ab-2=5');
|
||||||
|
$result = $this->executeFrontendRequest($request);
|
||||||
|
|
||||||
|
self::assertSame(200, $result->getStatusCode());
|
||||||
|
self::assertStringContainsString('Page 2 Title (Variant A)', $result->getBody()->__toString());
|
||||||
|
$this->assertPageIsNotCached($result);
|
||||||
|
$this->assertCookie($result, 'ab-2', '2');
|
||||||
|
$this->assertCounterOfPage(2, 1);
|
||||||
|
$this->assertCounterOfPage(5, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function opensVariantBForSecondVisitorIfVariantFromCookieDoesNotMatchVariantB(): void
|
||||||
|
{
|
||||||
|
$this->opensVariantAForFirstVisitor();
|
||||||
|
|
||||||
|
$request = new InternalRequest();
|
||||||
|
$request = $request->withPageId(2);
|
||||||
|
$request = $request->withAddedHeader('Cookie', 'ab-2=5');
|
||||||
|
$result = $this->executeFrontendRequest($request);
|
||||||
|
|
||||||
|
self::assertSame(200, $result->getStatusCode());
|
||||||
|
self::assertStringContainsString('Page 3 Title (Variant B)', $result->getBody()->__toString());
|
||||||
|
$this->assertPageIsNotCached($result);
|
||||||
|
$this->assertCookie($result, 'ab-2', '3');
|
||||||
|
$this->assertCounterOfPage(2, 1);
|
||||||
|
$this->assertCounterOfPage(3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function cookieHasDefaultLifetime(): void
|
||||||
|
{
|
||||||
|
$request = new InternalRequest();
|
||||||
|
$request = $request->withPageId(2);
|
||||||
|
$result = $this->executeFrontendRequest($request);
|
||||||
|
|
||||||
|
self::assertSame(200, $result->getStatusCode());
|
||||||
|
$cookie = Cookie::fromString($result->getHeaderLine('Set-Cookie'));
|
||||||
|
self::assertSame(604800, $cookie->getMaxAge());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function cookieHasConfiguredLifetime(): void
|
||||||
|
{
|
||||||
|
$request = new InternalRequest();
|
||||||
|
$request = $request->withPageId(4);
|
||||||
|
$result = $this->executeFrontendRequest($request);
|
||||||
|
|
||||||
|
self::assertSame(200, $result->getStatusCode());
|
||||||
|
$cookie = Cookie::fromString($result->getHeaderLine('Set-Cookie'));
|
||||||
|
self::assertSame(2419200, $cookie->getMaxAge());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure TYPO3 caching works as expected.
|
||||||
|
* The first call should create a proper cache entry.
|
||||||
|
* We should still be able to retrieve the other variant by adding the cookie.
|
||||||
|
* The 2nd variant should also be delivered from cache on 2nd request.
|
||||||
|
*
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function returnsCachedPage(): void
|
||||||
|
{
|
||||||
|
$request = new InternalRequest();
|
||||||
|
$request = $request->withPageId(2);
|
||||||
|
$result = $this->executeFrontendRequest($request);
|
||||||
|
self::assertStringContainsString('Page 2 Title (Variant A)', $result->getBody()->__toString());
|
||||||
|
$this->assertPageIsNotCached($result);
|
||||||
|
|
||||||
|
$request = new InternalRequest();
|
||||||
|
$request = $request->withPageId(2);
|
||||||
|
$request = $request->withAddedHeader('Cookie', 'ab-2=2');
|
||||||
|
$result = $this->executeFrontendRequest($request);
|
||||||
|
self::assertStringContainsString('Page 2 Title (Variant A)', $result->getBody()->__toString());
|
||||||
|
$this->assertPageIsCached($result);
|
||||||
|
|
||||||
|
$request = new InternalRequest();
|
||||||
|
$request = $request->withPageId(2);
|
||||||
|
$result = $this->executeFrontendRequest($request);
|
||||||
|
self::assertStringContainsString('Page 3 Title (Variant B)', $result->getBody()->__toString());
|
||||||
|
$this->assertPageIsNotCached($result);
|
||||||
|
|
||||||
|
$request = new InternalRequest();
|
||||||
|
$request = $request->withPageId(2);
|
||||||
|
$request = $request->withAddedHeader('Cookie', 'ab-2=3');
|
||||||
|
$result = $this->executeFrontendRequest($request);
|
||||||
|
self::assertStringContainsString('Page 3 Title (Variant B)', $result->getBody()->__toString());
|
||||||
|
$this->assertPageIsCached($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assertCounterOfPage(
|
||||||
|
int $pageUid,
|
||||||
|
int $expectedCounter,
|
||||||
|
string $message = ''
|
||||||
|
): void {
|
||||||
|
$actualCounter = $this->getConnectionPool()
|
||||||
|
->getConnectionForTable('pages')
|
||||||
|
->select(['tx_abtest_counter'], 'pages', ['uid' => $pageUid])
|
||||||
|
->fetchFirstColumn()[0] ?? 0
|
||||||
|
;
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
$expectedCounter,
|
||||||
|
$actualCounter,
|
||||||
|
'Counter for page ' . $pageUid . ' was not as expected. ' . $message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assertCookie(
|
||||||
|
InternalResponse $result,
|
||||||
|
string $name,
|
||||||
|
string $value
|
||||||
|
): void {
|
||||||
|
$cookie = Cookie::fromString($result->getHeaderLine('Set-Cookie'));
|
||||||
|
self::assertSame($name, $cookie->getName());
|
||||||
|
self::assertSame($value, $cookie->getValue());
|
||||||
|
self::assertSame('/', $cookie->getPath());
|
||||||
|
self::assertSame('lax', $cookie->getSameSite());
|
||||||
|
self::assertNull($cookie->getDomain());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assertCookieWasNotSet(InternalResponse $result): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
'',
|
||||||
|
$result->getHeaderLine('Set-Cookie'),
|
||||||
|
'Cookie was set but was not expected to be set.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assertPageIsNotCached(InternalResponse $result): void
|
||||||
|
{
|
||||||
|
self::assertSame('', $result->getHeaderLine('X-TYPO3-Debug-Cache'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assertPageIsCached(InternalResponse $result): void
|
||||||
|
{
|
||||||
|
self::assertStringStartsWith('Cached page generated', $result->getHeaderLine('X-TYPO3-Debug-Cache'));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,77 @@
|
||||||
{
|
{
|
||||||
"name": "svewap/abtest2",
|
"name": "werkraummedia/abtest",
|
||||||
"type": "typo3-cms-extension",
|
"type": "typo3-cms-extension",
|
||||||
"description": "",
|
"license": "GPL-2.0-or-later",
|
||||||
"authors": [],
|
"description": "Provides A/B Testing for TYPO3.",
|
||||||
|
"homepage": "https://github.com/werkraum-media/abtest",
|
||||||
|
"support": {
|
||||||
|
"docs": "https://docs.typo3.org/p/werkraummedia/abtest/master/en-us/",
|
||||||
|
"email": "coding@daniel-siepmann.de",
|
||||||
|
"issues": "https://github.com/werkraum-media/abtest/issues",
|
||||||
|
"source": "https://github.com/werkraum-media/abtest"
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Sven Wappler",
|
||||||
|
"email": "typo3YYYY@wappler.systems",
|
||||||
|
"homepage": "https://wappler.systems/",
|
||||||
|
"role": "Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Daniel Siepmann",
|
||||||
|
"email": "coding@daniel-siepmann.de",
|
||||||
|
"homepage": "https://daniel-siepmann.de/",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"typo3/cms-core": "^8.7.1"
|
"php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0",
|
||||||
|
"typo3/cms-core": "^11.5",
|
||||||
|
"typo3/cms-frontend": "^11.5",
|
||||||
|
"matomo/device-detector": "^6.1",
|
||||||
|
"symfony/http-foundation": "^5.4",
|
||||||
|
"psr/http-message": "^1.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "^1.10",
|
||||||
|
"typo3/testing-framework": "^6.16",
|
||||||
|
"phpunit/phpunit": "^9.6",
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.14",
|
||||||
|
"cweagans/composer-patches": "^1.7"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"WapplerSystems\\ABTest2\\": "Classes"
|
"WerkraumMedia\\ABTest\\": "Classes/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"WerkraumMedia\\ABTest\\Tests\\": "Tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"typo3/cms": {
|
||||||
|
"cms-package-dir": "{$vendor-dir}/typo3/cms",
|
||||||
|
"extension-key": "abtest",
|
||||||
|
"web-dir": ".Build/web"
|
||||||
|
},
|
||||||
|
"patches": {
|
||||||
|
"typo3/testing-framework": {
|
||||||
|
"Allow to test requests with cookies": "patches/testing-framework-cookies.patch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"post-autoload-dump": [
|
||||||
|
"@php -r 'is_dir($extFolder=__DIR__.\"/.Build/web/typo3conf/ext/\") || mkdir($extFolder, 0777, true);'",
|
||||||
|
"@php -r 'file_exists($extFolder=__DIR__.\"/.Build/web/typo3conf/ext/abtest\") || symlink(__DIR__,$extFolder);'"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": {
|
||||||
|
"typo3/class-alias-loader": true,
|
||||||
|
"typo3/cms-composer-installers": true,
|
||||||
|
"cweagans/composer-patches": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/***************************************************************
|
|
||||||
* Extension Manager/Repository config file for ext "abtest2".
|
|
||||||
*
|
|
||||||
* Auto generated 04-12-2017 10:08
|
|
||||||
*
|
|
||||||
* Manual updates:
|
|
||||||
* Only the data in the array - everything else is removed by next
|
|
||||||
* writing. "version" and "dependencies" must not be touched!
|
|
||||||
***************************************************************/
|
|
||||||
|
|
||||||
$EM_CONF[$_EXTKEY] = [
|
|
||||||
'title' => 'AB Test Pages',
|
|
||||||
'description' => 'With this extension, administrators can deliver different content for the same URL (AB test), depending on cookies or least showed page to realize a most accurate possible test.',
|
|
||||||
'category' => 'misc',
|
|
||||||
'author' => 'Sven Wappler',
|
|
||||||
'author_email' => 'typo3YYYY@wappler.systems',
|
|
||||||
'author_company' => 'WapplerSystems',
|
|
||||||
'state' => 'stable',
|
|
||||||
'uploadfolder' => false,
|
|
||||||
'createDirs' => '',
|
|
||||||
'clearCacheOnLoad' => true,
|
|
||||||
'version' => '1.0.0',
|
|
||||||
'constraints' =>
|
|
||||||
[
|
|
||||||
'depends' =>
|
|
||||||
[
|
|
||||||
'typo3' => '7.6.0-9.5.99',
|
|
||||||
],
|
|
||||||
'conflicts' =>
|
|
||||||
[],
|
|
||||||
'suggests' =>
|
|
||||||
[],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
|
@ -2,5 +2,4 @@
|
||||||
|
|
||||||
defined('TYPO3_MODE') or die();
|
defined('TYPO3_MODE') or die();
|
||||||
|
|
||||||
|
\WerkraumMedia\ABTest\Switcher::register();
|
||||||
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PostProc']['abtest2'] = 'WapplerSystems\\ABTest2\\Helper->determineContentId';
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
<?php
|
|
||||||
defined('TYPO3_MODE') || die('Access denied.');
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
CREATE TABLE pages (
|
CREATE TABLE pages (
|
||||||
tx_abtest2_b_id int(11) DEFAULT '0' NOT NULL,
|
tx_abtest_variant int(11) DEFAULT '0' NOT NULL,
|
||||||
tx_abtest2_cookie_time int(11) DEFAULT '86400' NOT NULL,
|
tx_abtest_cookie_time int(11) DEFAULT 604800 NOT NULL,
|
||||||
tx_abtest2_header text,
|
tx_abtest_counter int(11) DEFAULT '0' NOT NULL
|
||||||
tx_abtest2_footer text,
|
);
|
||||||
tx_abtest2_counter int(11) DEFAULT '0' NOT NULL
|
|
||||||
);
|
|
||||||
|
|
19
patches/testing-framework-cookies.patch
Normal file
19
patches/testing-framework-cookies.patch
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
See: https://github.com/TYPO3/testing-framework/issues/36
|
||||||
|
|
||||||
|
diff --git a/Classes/Core/Functional/Framework/Frontend/RequestBootstrap.php b/Classes/Core/Functional/Framework/Frontend/RequestBootstrap.php
|
||||||
|
index f8b2a40..4f4020d 100644
|
||||||
|
--- a/Classes/Core/Functional/Framework/Frontend/RequestBootstrap.php
|
||||||
|
+++ b/Classes/Core/Functional/Framework/Frontend/RequestBootstrap.php
|
||||||
|
@@ -106,6 +106,12 @@ class RequestBootstrap
|
||||||
|
}
|
||||||
|
// Populating $_COOKIE
|
||||||
|
$_COOKIE = [];
|
||||||
|
+ if ($this->request->hasHeader('Cookie')) {
|
||||||
|
+ foreach ($this->request->getHeader('Cookie') as $cookie) {
|
||||||
|
+ [$cookieName, $cookieValue] = explode('=', $cookie, 2);
|
||||||
|
+ $_COOKIE[$cookieName] = rtrim($cookieValue, ';');
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
|
||||||
|
// Setting up the server environment
|
||||||
|
$_SERVER = [];
|
9
phpstan.neon
Normal file
9
phpstan.neon
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
parameters:
|
||||||
|
level: max
|
||||||
|
paths:
|
||||||
|
- Classes
|
||||||
|
- Configuration
|
||||||
|
- Tests
|
||||||
|
checkMissingIterableValueType: false
|
||||||
|
reportUnmatchedIgnoredErrors: false
|
||||||
|
checkGenericClassInNonGenericObjectType: false
|
35
phpunit.xml.dist
Normal file
35
phpunit.xml.dist
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<phpunit
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
|
||||||
|
backupGlobals="false"
|
||||||
|
backupStaticAttributes="false"
|
||||||
|
bootstrap="vendor/typo3/testing-framework/Resources/Core/Build/FunctionalTestsBootstrap.php"
|
||||||
|
colors="true"
|
||||||
|
convertErrorsToExceptions="true"
|
||||||
|
convertWarningsToExceptions="true"
|
||||||
|
forceCoversAnnotation="false"
|
||||||
|
processIsolation="false"
|
||||||
|
stopOnError="false"
|
||||||
|
stopOnFailure="false"
|
||||||
|
stopOnIncomplete="false"
|
||||||
|
stopOnSkipped="false"
|
||||||
|
verbose="false"
|
||||||
|
>
|
||||||
|
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="functional">
|
||||||
|
<directory>Tests/Functional/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<coverage>
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">Classes</directory>
|
||||||
|
</include>
|
||||||
|
</coverage>
|
||||||
|
|
||||||
|
<php>
|
||||||
|
<env name="typo3DatabaseDriver" value="pdo_sqlite"/>
|
||||||
|
</php>
|
||||||
|
</phpunit>
|
83
shell.nix
Normal file
83
shell.nix
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
{ pkgs ? import <nixpkgs> { } }:
|
||||||
|
|
||||||
|
let
|
||||||
|
|
||||||
|
php = pkgs.php81.buildEnv {
|
||||||
|
extensions = { enabled, all }: enabled ++ (with all; [
|
||||||
|
xdebug
|
||||||
|
]);
|
||||||
|
|
||||||
|
extraConfig = ''
|
||||||
|
xdebug.mode = debug
|
||||||
|
|
||||||
|
memory_limit = 4G
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
composer = pkgs.php81Packages.composer.override {
|
||||||
|
inherit php;
|
||||||
|
};
|
||||||
|
|
||||||
|
projectInstall = pkgs.writeShellApplication {
|
||||||
|
name = "project-install";
|
||||||
|
runtimeInputs = [
|
||||||
|
php
|
||||||
|
composer
|
||||||
|
];
|
||||||
|
text = ''
|
||||||
|
composer install --no-interaction --prefer-dist --no-progress --working-dir="$PROJECT_ROOT"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
projectValidateComposer = pkgs.writeShellApplication {
|
||||||
|
name = "project-validate-composer";
|
||||||
|
runtimeInputs = [
|
||||||
|
php
|
||||||
|
composer
|
||||||
|
];
|
||||||
|
text = ''
|
||||||
|
composer validate
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
projectValidateXml = pkgs.writeShellApplication {
|
||||||
|
name = "project-validate-xml";
|
||||||
|
runtimeInputs = [
|
||||||
|
pkgs.libxml2
|
||||||
|
pkgs.wget
|
||||||
|
projectInstall
|
||||||
|
];
|
||||||
|
text = ''
|
||||||
|
project-install
|
||||||
|
xmllint --schema vendor/phpunit/phpunit/phpunit.xsd --noout phpunit.xml.dist
|
||||||
|
wget --no-check-certificate https://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd --output-document=xliff-core-1.2-strict.xsd
|
||||||
|
# shellcheck disable=SC2046
|
||||||
|
xmllint --schema xliff-core-1.2-strict.xsd --noout $(find Resources -name '*.xlf')
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
projectCodingGuideline = pkgs.writeShellApplication {
|
||||||
|
name = "project-coding-guideline";
|
||||||
|
runtimeInputs = [
|
||||||
|
php
|
||||||
|
projectInstall
|
||||||
|
];
|
||||||
|
text = ''
|
||||||
|
project-install
|
||||||
|
./vendor/bin/php-cs-fixer fix --dry-run --diff
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
in pkgs.mkShell {
|
||||||
|
name = "TYPO3 Extension abtest";
|
||||||
|
buildInputs = [
|
||||||
|
php
|
||||||
|
composer
|
||||||
|
|
||||||
|
projectValidateComposer
|
||||||
|
projectValidateXml
|
||||||
|
projectCodingGuideline
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
export PROJECT_ROOT="$(pwd)"
|
||||||
|
|
||||||
|
export typo3DatabaseDriver=pdo_sqlite
|
||||||
|
'';
|
||||||
|
}
|
Loading…
Reference in a new issue