Commit b55e1466 authored by Oliver Hader's avatar Oliver Hader Committed by Benni Mack
Browse files

[BUGFIX] Correctly retrieve workspace versions

* Clipboard now correctly resolves record localizations of a workspace
* PageLayoutController new correctly determines sub-pages that are new
  in a particular workspace
* SlugHelper & TypoScriptTemplateModuleController can be simplified
  by using WorkspaceRestriction directly
* common function test scenario tree (based on YAML) is introduced
  for ext:backend in order to be used as structure for other tests
* required testing framework changes support version and language
  variants and combination much better now

Resolves: #89138
Releases: master
Change-Id: Ia4b412d48dd3ea92adc60c729ad6feb27c22b812
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/61663


Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: default avatarDaniel Gorges <daniel.gorges@b13.com>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: default avatarDaniel Gorges <daniel.gorges@b13.com>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Achim Fritz's avatarAchim Fritz <af@achimfritz.de>
parent 2d8a9567
......@@ -76,7 +76,7 @@
"friendsofphp/php-cs-fixer": "^2.15.2",
"phpspec/prophecy": "^1.7.5",
"typo3/cms-styleguide": "~10.0.2",
"typo3/testing-framework": "~5.0.12"
"typo3/testing-framework": "~5.0.13"
},
"suggest": {
"ext-gd": "GDlib/Freetype is required for building images with text (GIFBUILDER) and can also be used to scale images",
......
......@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "7fe5f08c96831734ec662923a866edd5",
"content-hash": "29101a84cc5689c07e8b81dd4f0f2ada",
"packages": [
{
"name": "cogpowered/finediff",
......@@ -5017,8 +5017,8 @@
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"role": "lead"
"role": "lead",
"email": "sebastian@phpunit.de"
}
],
"description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
......@@ -5159,8 +5159,8 @@
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"role": "lead"
"role": "lead",
"email": "sebastian@phpunit.de"
}
],
"description": "Utility class for timing",
......@@ -5289,8 +5289,8 @@
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"role": "lead"
"role": "lead",
"email": "sebastian@phpunit.de"
}
],
"description": "The PHP Unit Testing framework.",
......@@ -5866,8 +5866,8 @@
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"role": "lead"
"role": "lead",
"email": "sebastian@phpunit.de"
}
],
"description": "Collection of value objects that represent the types of the PHP type system",
......@@ -6408,16 +6408,16 @@
},
{
"name": "typo3/testing-framework",
"version": "5.0.12",
"version": "5.0.13",
"source": {
"type": "git",
"url": "https://github.com/TYPO3/testing-framework.git",
"reference": "1fe51765371fa91bd8d54fbfbf527eff5c0b25d8"
"reference": "883abec7d68cb71a74627ef6dde82d65766af4da"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/TYPO3/testing-framework/zipball/1fe51765371fa91bd8d54fbfbf527eff5c0b25d8",
"reference": "1fe51765371fa91bd8d54fbfbf527eff5c0b25d8",
"url": "https://api.github.com/repos/TYPO3/testing-framework/zipball/883abec7d68cb71a74627ef6dde82d65766af4da",
"reference": "883abec7d68cb71a74627ef6dde82d65766af4da",
"shasum": ""
},
"require": {
......@@ -6464,7 +6464,7 @@
"tests",
"typo3"
],
"time": "2019-09-04T06:16:59+00:00"
"time": "2019-09-10T21:46:52+00:00"
}
],
"aliases": [],
......
......@@ -18,6 +18,7 @@ use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Resource\ResourceFactory;
......@@ -474,6 +475,8 @@ class Clipboard
{
$lines = [];
$tcaCtrl = $GLOBALS['TCA'][$table]['ctrl'];
$workspaceId = (int)$this->getBackendUser()->workspace;
if (BackendUtility::isTableLocalizable($table)) {
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
$queryBuilder->getRestrictions()
......@@ -499,13 +502,9 @@ class Clipboard
);
if (BackendUtility::isTableWorkspaceEnabled($table)) {
$queryBuilder
->andWhere(
$queryBuilder->expr()->eq(
't3ver_wsid',
$queryBuilder->createNamedParameter($parentRec['t3ver_wsid'], \PDO::PARAM_INT)
)
);
$queryBuilder->getRestrictions()->add(
GeneralUtility::makeInstance(WorkspaceRestriction::class, $workspaceId)
);
}
$rows = $queryBuilder->execute()->fetchAll();
if (is_array($rows)) {
......
......@@ -29,6 +29,7 @@ use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Imaging\Icon;
......@@ -1216,30 +1217,21 @@ class PageLayoutController
*/
protected function currentPageHasSubPages(): bool
{
// get workspace id
$workspaceId = (int)$this->getBackendUser()->workspace;
/** @var QueryBuilder $queryBuilder */
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
$queryBuilder->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
// get workspace id
$workspaceId = (int)$this->getBackendUser()->workspace;
$comparisonExpression = $workspaceId === 0 ? 'neq' : 'eq';
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $workspaceId));
$count = $queryBuilder
->count('uid')
->from('pages')
->where(
$queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq(
't3ver_wsid',
$queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
),
$queryBuilder->expr()->{$comparisonExpression}(
'pid',
$queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
)
$queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT))
)
->execute()
->fetchColumn(0);
......
<?php
namespace TYPO3\CMS\Backend\Tests\Functional\Clipboard;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Backend\Clipboard\Clipboard;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerFactory;
use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerWriter;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
/**
* Functional database test for Clipboard behaviour
*/
class ClipboardTest extends FunctionalTestCase
{
use SiteBasedTestTrait;
/**
* @var string[]
*/
protected $coreExtensionsToLoad = ['workspaces'];
/**
* @var Clipboard
*/
private $subject;
/**
* @var BackendUserAuthentication
*/
private $backendUser;
public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();
static::initializeDatabaseSnapshot();
}
public static function tearDownAfterClass(): void
{
static::destroyDatabaseSnapshot();
parent::tearDownAfterClass();
}
protected function setUp(): void
{
parent::setUp();
$this->subject = GeneralUtility::makeInstance(Clipboard::class);
$this->backendUser = $this->setUpBackendUserFromFixture(1);
$this->withDatabaseSnapshot(function () {
$this->setUpDatabase();
});
}
protected function setUpDatabase()
{
Bootstrap::initializeLanguageObject();
$scenarioFile = __DIR__ . '/../Fixtures/CommonScenario.yaml';
$factory = DataHandlerFactory::fromYamlFile($scenarioFile);
$writer = DataHandlerWriter::withBackendUser($this->backendUser);
$writer->invokeFactory($factory);
static::failIfArrayIsNotEmpty(
$writer->getErrors()
);
}
/**
* @return array
*/
public function localizationsAreResolvedDataProvider(): array
{
return [
'live workspace with live & version localizations' => [
1100,
0,
[
'FR: Welcome',
'FR-CA: Welcome',
]
],
'draft workspace with live & version localizations' => [
1100,
1,
[
'FR: Welcome',
'FR-CA: Welcome',
'ES: Bienvenido',
]
],
'live workspace with live localizations only' => [
1400,
0,
[
'FR: ACME in your Region',
'FR-CA: ACME in your Region',
]
],
'draft workspace with live localizations only' => [
1400,
1,
[
'FR: ACME in your Region',
'FR-CA: ACME in your Region',
]
],
'live workspace with version localizations only' => [
1500,
0,
[]
],
'draft workspace with version localizations only' => [
1500,
1,
[
'FR: Interne',
]
],
];
}
/**
* @param int $pageId
* @param int $workspaceId
* @param array $expectation
*
* @dataProvider localizationsAreResolvedDataProvider
* @test
*/
public function localizationsAreResolved(int $pageId, int $workspaceId, array $expectation)
{
$this->backendUser->workspace = $workspaceId;
$record = BackendUtility::getRecordWSOL('pages', $pageId);
$actualResult = array_column(
$this->subject->getLocalizations('pages', $record),
'title'
);
static::assertSame($expectation, $actualResult);
}
}
<?php
namespace TYPO3\CMS\Backend\Tests\Functional\Controller;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Backend\Controller\PageLayoutController;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\TestingFramework\Core\AccessibleObjectInterface;
use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerFactory;
use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerWriter;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
/**
* Functional database test for PageLayoutController behaviour
*/
class PageLayoutControllerTest extends FunctionalTestCase
{
use SiteBasedTestTrait;
/**
* @var string[]
*/
protected $coreExtensionsToLoad = ['workspaces'];
/**
* @var PageLayoutController|AccessibleObjectInterface
*/
private $subject;
/**
* @var BackendUserAuthentication
*/
private $backendUser;
public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();
static::initializeDatabaseSnapshot();
}
public static function tearDownAfterClass(): void
{
static::destroyDatabaseSnapshot();
parent::tearDownAfterClass();
}
protected function setUp(): void
{
parent::setUp();
$className = $this->buildAccessibleProxy(PageLayoutController::class);
$this->subject = GeneralUtility::makeInstance($className);
$this->backendUser = $this->setUpBackendUserFromFixture(1);
$this->withDatabaseSnapshot(function () {
$this->setUpDatabase();
});
}
protected function setUpDatabase()
{
Bootstrap::initializeLanguageObject();
$scenarioFile = __DIR__ . '/../Fixtures/CommonScenario.yaml';
$factory = DataHandlerFactory::fromYamlFile($scenarioFile);
$writer = DataHandlerWriter::withBackendUser($this->backendUser);
$writer->invokeFactory($factory);
static::failIfArrayIsNotEmpty(
$writer->getErrors()
);
}
/**
* @return array
*/
public function currentPageHasSubPagesDataProvider(): array
{
return [
'live workspace with live & version sub pages' => [
1000,
0,
true
],
'draft workspace with live & version sub pages' => [
1000,
1,
true
],
'live workspace with version sub pages only' => [
1950,
0,
false
],
'draft workspace with version sub pages only' => [
1950,
1,
true
],
'live workspace with live sub pages only' => [
2000,
0,
true
],
'draft workspace with live sub pages only' => [
2000,
1,
true
],
];
}
/**
* @param int $pageId
* @param int $workspaceId
* @param bool $expectation
*
* @dataProvider currentPageHasSubPagesDataProvider
* @test
*/
public function currentPageHasSubPages(int $pageId, int $workspaceId, bool $expectation)
{
$this->backendUser->workspace = $workspaceId;
$this->subject->_set('id', $pageId);
$actualResult = $this->subject->_call('currentPageHasSubPages');
static::assertSame($expectation, $actualResult);
}
}
__variables:
- &pageStandard 0
- &pageShortcut 4
- &pageMount 7
- &pageFolder 254
- &contentText 'text'
- &idAcmeRootPage 1000
- &idAcmeFirstPage 1100
entitySettings:
'*':
nodeColumnName: 'pid'
columnNames: {id: 'uid', language: 'sys_language_uid'}
defaultValues: {pid: 0}
page:
isNode: true
tableName: 'pages'
parentColumnName: 'pid'
languageColumnNames: ['l10n_parent', 'l10n_source']
columnNames: {type: 'doktype', root: 'is_siteroot', mount: 'mount_pid', visitorGroups: 'fe_group'}
defaultValues: {hidden: 0, doktype: *pageStandard}
valueInstructions:
shortcut:
first: {shortcut: 0, shortcut_mode: 1}
content:
tableName: 'tt_content'
languageColumnNames: ['l18n_parent', 'l10n_source']
columnNames: {title: 'header', type: 'CType'}
workspace:
tableName: 'sys_workspace'
language:
tableName: 'sys_language'
columnNames: {code: 'language_isocode'}
visitorGroup:
tableName: 'fe_groups'
visitor:
tableName: 'fe_users'
columnNames: {groups: 'usergroup'}
typoscript:
tableName: 'sys_template'
valueInstructions:
type:
site: {root: 1, clear: 1}
entities:
workspace:
- self: {id: 1, title: 'Workspace'}
language:
- self: {id: 1, title: 'French', code: 'fr'}
- self: {id: 2, title: 'Franco-Canadian', code: 'fr'}
- self: {id: 3, title: 'Spanish', code: 'es'}
page:
- self: {id: *idAcmeRootPage, title: 'ACME Inc', type: *pageShortcut, shortcut: 'first', root: true, slug: '/'}
children:
- self: {id: *idAcmeFirstPage, title: 'EN: Welcome', slug: '/welcome', subtitle: 'hello-and-welcome'}
languageVariants:
- self: {id: 1101, title: 'FR: Welcome', language: 1, slug: '/bienvenue', subtitle: 'salut-et-bienvenue'}
- self: {id: 1102, title: 'FR-CA: Welcome', language: 2, slug: '/bienvenue', subtitle: 'salut-et-bienvenue'}
- version: {id: 1103, title: 'ES: Bienvenido', workspace: 1, language: 3, slug: '/bienvenido', subtitle: 'bienvenido'}
versionVariants:
- version: {title: 'EN: Welcome to ACME Inc', workspace: 1, slug: '/welcome-modified'}
entities:
content:
- self: {title: 'EN: Content Element #1', type: *contentText}
# @todo does not work due to a bug in DataHandler's remap stack for l10n_source
languageVariants:
- self: {title: 'FR: Content Element #1', type: *contentText, language: 1}
languageVariants:
- self: {title: 'FR-CA: Content Element #1', type: *contentText, language: 2}
- self: {title: 'EN: Content Element #2', type: *contentText}
- self: {id: 1200, title: 'EN: Features', slug: '/features'}
children:
- self: {id: 1210, title: 'EN: Frontend Editing', slug: '/features/frontend-editing'}
- self: {id: 1300, title: 'EN: Products', root: true, slug: '/products'}
children:
- self: {id: 1310, title: 'EN: Planets', slug: '/products/planets'}
- self: {id: 1320, title: 'EN: Spaceships', slug: '/products/spaceships'}
- self: {id: 1330, title: 'EN: Dark Matter', slug: '/products/dark-matter'}
- self: {id: 1400, title: 'EN: ACME in your Region', root: true, slug: '/acme-in-your-region'}
languageVariants:
- self: {id: 1401, title: 'FR: ACME in your Region', language: 1, slug: '/acme-dans-votre-region'}
- self: {id: 1402, title: 'FR-CA: ACME in your Region', language: 2, slug: '/acme-dans-votre-quebec'}
children:
- self: {id: 1410, title: 'EN: Groups', slug: '/acme-in-your-region/groups', l18n_cfg: 1}
languageVariants:
- self: {id: 1411, title: 'FR: Groups', language: 1, slug: '/acme-dans-votre-region/groupes'}
- self: {id: 1412, title: 'FR-CA: Groups', language: 2, slug: '/acme-dans-votre-quebec/groupes'}
- self: {id: 1500, title: 'Internal', slug: '/my-acme'}
children:
- self: {id: 1510, title: 'Whitepapers', visitorGroups: -2, extendToSubpages: true, slug: '/my-acme/whitepapers'}
children:
- self: {id: 1511, title: 'Products', slug: '/my-acme/whitepapers/products'}
- self: {id: 1512, title: 'Solutions', visitorGroups: 10, slug: '/my-acme/whitepapers/solutions'}
- self: {id: 1515, title: 'Research', visitorGroups: 20, slug: '/my-acme/whitepapers/research'}
- self: {id: 1520, title: 'Forecasts', visitorGroups: 20, extendToSubpages: true, slug: '/my-acme/forecasts'}
children:
- self: {id: 1521, title: 'Current Year', slug: '/my-acme/forecasts/current-year'}
- self: {id: 1522, title: 'Next Year', slug: '/my-acme/forecasts/next-year'}
- self: {id: 1523, title: 'Five Years', slug: '/my-acme/forecasts/five-years'}
languageVariants:
- version: {title: 'FR: Interne', workspace: 1, language: 1, slug: '/my-acme'}
- self: {id: 1600, title: 'About us', slug: '/about'}
- self: {id: 1700, title: 'Announcements & News', type: *pageMount, mount: 7100, slug: '/news'}
- self: {id: 404, title: 'Page not found', slug: '/404'}
entities:
content:
- self: {title: 'EN: Page not found', type: *contentText}
- self: {id: 1930, title: 'Our Blog', type: *pageShortcut, shortcut: 2000, slug: '/blog'}
- version: {id: 1950, title: 'EN: Goodbye', workspace: 1, slug: '/bye'}
children:
- version: {title: 'EN: Really Goodbye', workspace: 1, slug: '/bye/bye'}
- self: {id: 1990, title: 'Storage', type: *pageFolder, slug: '/internal/storage'}
entities:
visitorGroup:
- self: {id: 10, title: 'Customers'}