Compare commits

..

No commits in common. "5.2.0-beta1" and "4" have entirely different histories.

172 changed files with 13248 additions and 8164 deletions

View File

@ -1 +0,0 @@
<!-- Blank templates are for use by maintainers only! If you aren't a maintainer, please go back and pick one of the issue templates. -->

View File

@ -1,72 +0,0 @@
name: 🪳 Bug Report
description: Tell us if something isn't working the way it's supposed to
body:
- type: markdown
attributes:
value: |
We strongly encourage you to [submit a pull request](https://docs.silverstripe.org/en/contributing/code/) which fixes the issue.
Bug reports which are accompanied with a pull request are a lot more likely to be resolved quickly.
- type: input
id: affected-versions
attributes:
label: Module version(s) affected
description: |
What version of _this module_ have you reproduced this bug on?
Run `composer info` to see the specific version of each module installed in your project.
If you don't have access to that, check inside the help menu in the bottom left of the CMS.
placeholder: x.y.z
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: A clear and concise description of the problem
validations:
required: true
- type: textarea
id: how-to-reproduce
attributes:
label: How to reproduce
description: |
⚠️ This is the most important part of the report ⚠️
Without a way to easily reproduce your issue, there is little chance we will be able to help you and work on a fix.
- Please, take the time to show us some code and/or configuration that is needed for others to reproduce the problem easily.
- If the bug is too complex to reproduce with some short code samples, please reproduce it in a public repository and provide a link to the repository along with steps for setting up and reproducing the bug using that repository.
- If part of the bug includes an error or exception, please provide a full stack trace.
- If any user interaction is required to reproduce the bug, please add an ordered list of steps that are required to reproduce it.
- Be as clear as you can, but don't miss any steps out. Simply saying "create a page" is less useful than guiding us through the steps you're taking to create a page, for example.
placeholder: |
#### Code sample
```php
```
#### Reproduction steps
1.
validations:
required: true
- type: textarea
id: possible-solution
attributes:
label: Possible Solution
description: |
*Optional: only if you have suggestions on a fix/reason for the bug*
Please consider [submitting a pull request](https://docs.silverstripe.org/en/contributing/code/) with your solution! It helps get faster feedback and greatly increases the chance of the bug being fixed.
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: "*Optional: any other context about the problem: log messages, screenshots, etc.*"
- type: checkboxes
id: validations
attributes:
label: Validations
description: "Before submitting the issue, please make sure you do the following:"
options:
- label: Check that there isn't already an issue that reports the same bug
required: true
- label: Double check that your reproduction steps work in a fresh installation of [`silverstripe/installer`](https://github.com/silverstripe/silverstripe-installer) (with any code examples you've provided)
required: true

View File

@ -1,35 +0,0 @@
name: 🚀 Feature Request
description: Submit a feature request (but only if you're planning on implementing it)
body:
- type: markdown
attributes:
value: |
Please only submit feature requests if you plan on implementing the feature yourself.
See the [contributing code documentation](https://docs.silverstripe.org/en/contributing/code/#make-or-find-a-github-issue) for more guidelines about submitting feature requests.
- type: textarea
id: description
attributes:
label: Description
description: A clear and concise description of the new feature, and why it belongs in core
validations:
required: true
- type: textarea
id: more-info
attributes:
label: Additional context or points of discussion
description: |
*Optional: Any additional context, points of discussion, etc that might help validate and refine your idea*
- type: checkboxes
id: validations
attributes:
label: Validations
description: "Before submitting the issue, please confirm the following:"
options:
- label: You intend to implement the feature yourself
required: true
- label: You have read the [contributing guide](https://docs.silverstripe.org/en/contributing/code/)
required: true
- label: You strongly believe this feature should be in core, rather than being its own community module
required: true
- label: You have checked for existing issues or pull requests related to this feature (and didn't find any)
required: true

View File

@ -1,8 +0,0 @@
blank_issues_enabled: true
contact_links:
- name: Security Vulnerability
url: https://docs.silverstripe.org/en/contributing/issues_and_bugs/#reporting-security-issues
about: ⚠️ We do not use GitHub issues to track security vulnerability reports. Click "open" on the right to see how to report security vulnerabilities.
- name: Support Question
url: https://www.silverstripe.org/community/
about: We use GitHub issues only to discuss bugs and new features. For support questions, please use one of the support options available in our community channels.

View File

@ -1,39 +0,0 @@
<!--
Thanks for contributing, you're awesome! ⭐
Please read https://docs.silverstripe.org/en/contributing/code/ if you haven't contributed to this project recently.
-->
## Description
<!--
Please describe expected and observed behaviour, and what you're fixing.
For visual fixes, please include tested browsers and screenshots.
-->
## Manual testing steps
<!--
Include any manual testing steps here which a reviewer can perform to validate your pull request works correctly.
Note that this DOES NOT replace unit or end-to-end tests.
-->
## Issues
<!--
List all issues here that this pull request fixes/resolves.
If there is no issue already, create a new one! You must link your pull request to at least one issue.
-->
- #
## Pull request checklist
<!--
PLEASE check each of these to ensure you have done everything you need to do!
If there's something in this list you need help with, please ask so that we can assist you.
-->
- [ ] The target branch is correct
- See [picking the right version](https://docs.silverstripe.org/en/contributing/code/#picking-the-right-version)
- [ ] All commits are relevant to the purpose of the PR (e.g. no debug statements, unrelated refactoring, or arbitrary linting)
- Small amounts of additional linting are usually okay, but if it makes it hard to concentrate on the relevant changes, ask for the unrelated changes to be reverted, and submitted as a separate PR.
- [ ] The commit messages follow our [commit message guidelines](https://docs.silverstripe.org/en/contributing/code/#commit-messages)
- [ ] The PR follows our [contribution guidelines](https://docs.silverstripe.org/en/contributing/code/)
- [ ] Code changes follow our [coding conventions](https://docs.silverstripe.org/en/contributing/coding_conventions/)
- [ ] This change is covered with tests (or tests aren't necessary for this change)
- [ ] Any relevant User Help/Developer documentation is updated; for impactful changes, information is added to the changelog for the intended release
- [ ] CI is green

View File

@ -1,10 +1,10 @@
name: Keepalive
on:
# At 6:30 PM UTC, on day 15 of the month
schedule:
- cron: '30 18 15 * *'
workflow_dispatch:
# The 4th of every month at 10:50am UTC
schedule:
- cron: '50 10 4 * *'
jobs:
keepalive:

View File

@ -1,17 +0,0 @@
name: Merge-up
on:
# At 1:20 PM UTC, only on Saturday
schedule:
- cron: '20 13 * * 6'
workflow_dispatch:
jobs:
merge-up:
name: Merge-up
# Only run cron on the silverstripe account
if: (github.event_name == 'schedule' && github.repository_owner == 'silverstripe') || (github.event_name != 'schedule')
runs-on: ubuntu-latest
steps:
- name: Merge-up
uses: silverstripe/gha-merge-up@v1

View File

@ -4,7 +4,7 @@ on:
workflow_dispatch:
# Run on a schedule of once per quarter
schedule:
- cron: '30 18 1 */3 *'
- cron: '0 0 1 */3 *'
jobs:
update-js:

2
.nvmrc
View File

@ -1 +1 @@
18
10

161
.upgrade.yml Normal file
View File

@ -0,0 +1,161 @@
mappings:
CMSBatchAction_Publish: SilverStripe\CMS\BatchActions\CMSBatchAction_Publish
CMSBatchAction_Unpublish: SilverStripe\CMS\BatchActions\CMSBatchAction_Unpublish
CMSBatchAction_Archive: SilverStripe\CMS\BatchActions\CMSBatchAction_Archive
CMSBatchAction_Restore: SilverStripe\CMS\BatchActions\CMSBatchAction_Restore
CMSMain: SilverStripe\CMS\Controllers\CMSMain
CMSPageAddController: SilverStripe\CMS\Controllers\CMSPageAddController
CMSPageEditController: SilverStripe\CMS\Controllers\CMSPageEditController
CMSPageHistoryController: SilverStripe\CMS\Controllers\CMSPageHistoryController
CMSPagesController: SilverStripe\CMS\Controllers\CMSPagesController
CMSPageSettingsController: SilverStripe\CMS\Controllers\CMSPageSettingsController
CMSSiteTreeFilter: SilverStripe\CMS\Controllers\CMSSiteTreeFilter
CMSSiteTreeFilter_PublishedPages: SilverStripe\CMS\Controllers\CMSSiteTreeFilter_PublishedPages
CMSSiteTreeFilter_DeletedPages: SilverStripe\CMS\Controllers\CMSSiteTreeFilter_DeletedPages
CMSSiteTreeFilter_ChangedPages: SilverStripe\CMS\Controllers\CMSSiteTreeFilter_ChangedPages
CMSSiteTreeFilter_StatusRemovedFromDraftPages: SilverStripe\CMS\Controllers\CMSSiteTreeFilter_StatusRemovedFromDraftPages
CMSSiteTreeFilter_StatusDraftPages: SilverStripe\CMS\Controllers\CMSSiteTreeFilter_StatusDraftPages
CMSSiteTreeFilter_StatusDeletedPages: SilverStripe\CMS\Controllers\CMSSiteTreeFilter_StatusDeletedPages
CMSSiteTreeFilter_Search: SilverStripe\CMS\Controllers\CMSSiteTreeFilter_Search
ContentController: SilverStripe\CMS\Controllers\ContentController
ErrorPageControllerExtension: SilverStripe\ErrorPage\ErrorPageControllerExtension
ErrorPageFileExtension: SilverStripe\ErrorPage\ErrorPageFileExtension
LeftAndMainPageIconsExtension: SilverStripe\CMS\Controllers\LeftAndMainPageIconsExtension
ModelAsController: SilverStripe\CMS\Controllers\ModelAsController
OldPageRedirector: SilverStripe\CMS\Controllers\OldPageRedirector
RootURLController: SilverStripe\CMS\Controllers\RootURLController
SilverStripeNavigator: SilverStripe\CMS\Controllers\SilverStripeNavigator
SilverStripeNavigatorItem: SilverStripe\CMS\Controllers\SilverStripeNavigatorItem
SilverStripeNavigatorItem_CMSLink: SilverStripe\CMS\Controllers\SilverStripeNavigatorItem_CMSLink
SilverStripeNavigatorItem_StageLink: SilverStripe\CMS\Controllers\SilverStripeNavigatorItem_StageLink
SilverStripeNavigatorItem_LiveLink: SilverStripe\CMS\Controllers\SilverStripeNavigatorItem_LiveLink
SilverStripeNavigatorItem_ArchiveLink: SilverStripe\CMS\Controllers\SilverStripeNavigatorItem_ArchiveLink
SiteTreeURLSegmentField: SilverStripe\CMS\Forms\SiteTreeURLSegmentField
SiteTreeURLSegmentField_Readonly: SilverStripe\CMS\Forms\SiteTreeURLSegmentField_Readonly
CurrentPageIdentifier: SilverStripe\CMS\Model\CurrentPageIdentifier
ErrorPage: SilverStripe\ErrorPage\ErrorPage
ErrorPage_Controller: SilverStripe\ErrorPage\ErrorPageController
RedirectorPage: SilverStripe\CMS\Model\RedirectorPage
RedirectorPage_Controller: SilverStripe\CMS\Model\RedirectorPageController
SiteTree: SilverStripe\CMS\Model\SiteTree
SiteTreeExtension: SilverStripe\CMS\Model\SiteTreeExtension
SiteTreeFileExtension: SilverStripe\CMS\Model\SiteTreeFileExtension
SiteTreeFolderExtension: SilverStripe\CMS\Model\SiteTreeFolderExtension
SiteTreeLinkTracking: SilverStripe\CMS\Model\SiteTreeLinkTracking
SiteTreeLinkTracking_Parser: SilverStripe\CMS\Model\SiteTreeLinkTracking_Parser
VirtualPage: SilverStripe\CMS\Model\VirtualPage
BrokenFilesReport: SilverStripe\CMS\Reports\BrokenFilesReport
SideReport_BrokenFiles: SilverStripe\CMS\Reports\BrokenFilesReport
BrokenLinksReport: SilverStripe\CMS\Reports\BrokenLinksReport
SideReport_BrokenLinks: SilverStripe\CMS\Reports\BrokenLinksReport
BrokenRedirectorPagesReport: SilverStripe\CMS\Reports\BrokenRedirectorPagesReport
SideReport_BrokenRedirectorPages: SilverStripe\CMS\Reports\BrokenRedirectorPagesReport
BrokenVirtualPagesReport: SilverStripe\CMS\Reports\BrokenVirtualPagesReport
SideReport_BrokenVirtualPages: SilverStripe\CMS\Reports\BrokenVirtualPagesReport
EmptyPagesReport: SilverStripe\CMS\Reports\EmptyPagesReport
SideReport_EmptyPages: SilverStripe\CMS\Reports\EmptyPagesReport
RecentlyEditedReport: SilverStripe\CMS\Reports\RecentlyEditedReport
SideReport_RecentlyEdited: SilverStripe\CMS\Reports\RecentlyEditedReport
ContentControllerSearchExtension: SilverStripe\CMS\Search\ContentControllerSearchExtension
SearchForm: SilverStripe\CMS\Search\SearchForm
MigrateSiteTreeLinkingTask: SilverStripe\CMS\Tasks\MigrateSiteTreeLinkingTask
RemoveOrphanedPagesTask: SilverStripe\CMS\Tasks\RemoveOrphanedPagesTask
SiteTreeMaintenanceTask: SilverStripe\CMS\Tasks\SiteTreeMaintenanceTask
AssetAdmin: SilverStripe\AssetAdmin\Controller\AssetAdmin
AssetTableField: SilverStripe\AssetAdmin\Controller\AssetAdmin
CMSAddPageController: SilverStripe\CMS\Controllers\CMSPageAddController
CMSBatchActions: SilverStripe\Admin\CMSBatchAction
CMSSearch: SilverStripe\CMS\Search\SearchForm
ContentControl: SilverStripe\CMS\Controllers\ContentController
Permissions: SilverStripe\Security\Permission
SITETREE: SilverStripe\CMS\Model\SiteTree
CMSMain_left_ss: SilverStripe\CMS\Controllers\CMSMain
CMSPageHistoryController_versions_ss: SilverStripe\CMS\Controllers\CMSPageHistoryController
CMSPagesController_ContentToolbar_ss: SilverStripe\CMS\Controllers\CMSPageHistoryController
CMSSIteTreeFilter_PublishedPages: SilverStripe\CMS\Controllers\CMSSIteTreeFilter_PublishedPages
URLSegmentField: SilverStripe\CMS\Forms\SiteTreeURLSegmentField
CMSPagesController_Tools_ss: SilverStripe\CMS\Controllers\CMSPagesController
LeftAndMain: SilverStripe\Admin\LeftAndMain
CMSBatchActionsTest: SilverStripe\CMS\Tests\CMSBatchActionsTest
CMSMainTest: SilverStripe\CMS\Tests\CMSMainTest
CMSMainTest_ClassA: SilverStripe\CMS\Tests\CMSMainTest_ClassA
CMSMainTest_ClassB: SilverStripe\CMS\Tests\CMSMainTest_ClassB
CMSMainTest_HiddenClass: SilverStripe\CMS\Tests\CMSMainTest_HiddenClass
CMSMainTest_NotRoot: SilverStripe\CMS\Tests\CMSMainTest_NotRoot
CMSPageHistoryControllerTest: SilverStripe\CMS\Tests\CMSPageHistoryControllerTest
CMSSiteTreeFilterTest: SilverStripe\CMS\Tests\CMSSiteTreeFilterTest
CMSTreeTest: SilverStripe\CMS\Tests\CMSTreeTest
ContentControllerPermissionsTest: SilverStripe\CMS\Tests\ContentControllerPermissionsTest
ContentControllerSearchExtensionTest: SilverStripe\CMS\Tests\ContentControllerSearchExtensionTest
ContentControllerTest: SilverStripe\CMS\Tests\ContentControllerTest
ContentControllerTest_Page: SilverStripe\CMS\Tests\ContentControllerTest_Page
ContentControllerTest_PageController: SilverStripe\CMS\Tests\ContentControllerTest_PageController
ContentControllerTestPage: SilverStripe\CMS\Tests\ContentControllerTestPage
ContentControllerTestPageController: SilverStripe\CMS\Tests\ContentControllerTestPageController
ContentControllerTestPageWithoutController: SilverStripe\CMS\Tests\ContentControllerTestPageWithoutController
ModelAsControllerTest: SilverStripe\CMS\Tests\ModelAsControllerTest
RootURLControllerTest: SilverStripe\CMS\Tests\RootURLControllerTest
SilverStripeNavigatorTest: SilverStripe\CMS\Tests\SilverStripeNavigatorTest
SilverStripeNavigatorTest_ProtectedTestItem: SilverStripe\CMS\Tests\SilverStripeNavigatorTest_ProtectedTestItem
SilverStripeNavigatorTest_TestItem: SilverStripe\CMS\Tests\SilverStripeNavigatorTest_TestItem
FileLinkTrackingTest: SilverStripe\CMS\Tests\FileLinkTrackingTest
RedirectorPageTest: SilverStripe\CMS\Tests\RedirectorPageTest
RedirectorPageTest_RedirectExtension: SilverStripe\CMS\Tests\RedirectorPageTest_RedirectExtension
SiteTreeActionsTest: SilverStripe\CMS\Tests\SiteTreeActionsTest
SiteTreeActionsTest_Page: SilverStripe\CMS\Tests\SiteTreeActionsTest_Page
SiteTreeBacklinksTest: SilverStripe\CMS\Tests\SiteTreeBacklinksTest
SiteTreeBacklinksTest_DOD: SilverStripe\CMS\Tests\SiteTreeBacklinksTest_DOD
SiteTreeBrokenLinksTest: SilverStripe\CMS\Tests\SiteTreeBrokenLinksTest
SiteTreeHTMLEditorFieldTest: SilverStripe\CMS\Tests\SiteTreeHTMLEditorFieldTest
SiteTreeLinkTrackingTest: SilverStripe\CMS\Tests\SiteTreeLinkTrackingTest
SiteTreePermissionsTest: SilverStripe\CMS\Tests\SiteTreePermissionsTest
SiteTreeTest: SilverStripe\CMS\Tests\SiteTreeTest
SiteTreeTest_AdminDenied: SilverStripe\CMS\Tests\SiteTreeTest_AdminDenied
SiteTreeTest_AdminDeniedExtension: SilverStripe\CMS\Tests\SiteTreeTest_AdminDeniedExtension
SiteTreeTest_ClassA: SilverStripe\CMS\Tests\SiteTreeTest_ClassA
SiteTreeTest_ClassB: SilverStripe\CMS\Tests\SiteTreeTest_ClassB
SiteTreeTest_ClassC: SilverStripe\CMS\Tests\SiteTreeTest_ClassC
SiteTreeTest_ClassCext: SilverStripe\CMS\Tests\SiteTreeTest_ClassCext
SiteTreeTest_ClassD: SilverStripe\CMS\Tests\SiteTreeTest_ClassD
SiteTreeTest_ClassE: SilverStripe\CMS\Tests\SiteTreeTest_ClassE
SiteTreeTest_Conflicted: SilverStripe\CMS\Tests\SiteTreeTest_Conflicted
SiteTreeTest_ConflictedController: SilverStripe\CMS\Tests\SiteTreeTest_ConflictedController
SiteTreeTest_DataObject: SilverStripe\CMS\Tests\SiteTreeTest_DataObject
SiteTreeTest_Extension: SilverStripe\CMS\Tests\SiteTreeTest_Extension
SiteTreeTest_ExtensionA: SilverStripe\CMS\Tests\SiteTreeTest_ExtensionA
SiteTreeTest_ExtensionB: SilverStripe\CMS\Tests\SiteTreeTest_ExtensionB
SiteTreeTest_LegacyControllerName: SilverStripe\CMS\Tests\SiteTreeTest_LegacyControllerName
SiteTreeTest_LegacyControllerName_Controller: SilverStripe\CMS\Tests\SiteTreeTest_LegacyControllerName_Controller
SiteTreeTest_NotRoot: SilverStripe\CMS\Tests\SiteTreeTest_NotRoot
SiteTreeTest_NullHtmlCleaner: SilverStripe\CMS\Tests\SiteTreeTest_NullHtmlCleaner
SiteTreeTest_PageNode: SilverStripe\CMS\Tests\SiteTreeTest_PageNode
SiteTreeTest_PageNodeController: SilverStripe\CMS\Tests\SiteTreeTest_PageNodeController
SiteTreeTest_StageStatusInherit: SilverStripe\CMS\Tests\SiteTreeTest_StageStatusInherit
VirtualPageTest: SilverStripe\CMS\Tests\VirtualPageTest
VirtualPageTest_ClassA: SilverStripe\CMS\Tests\VirtualPageTest_ClassA
VirtualPageTest_ClassAController: SilverStripe\CMS\Tests\VirtualPageTest_ClassAController
VirtualPageTest_ClassB: SilverStripe\CMS\Tests\VirtualPageTest_ClassB
VirtualPageTest_ClassC: SilverStripe\CMS\Tests\VirtualPageTest_ClassC
VirtualPageTest_NotRoot: SilverStripe\CMS\Tests\VirtualPageTest_NotRoot
VirtualPageTest_PageExtension: SilverStripe\CMS\Tests\VirtualPageTest_PageExtension
VirtualPageTest_PageWithAllowedChildren: SilverStripe\CMS\Tests\VirtualPageTest_PageWithAllowedChildren
VirtualPageTest_TestDBField: SilverStripe\CMS\Tests\VirtualPageTest_TestDBField
VirtualPageTest_VirtualPageSub: SilverStripe\CMS\Tests\VirtualPageTest_VirtualPageSub
CmsReportsTest: SilverStripe\CMS\Tests\CmsReportsTest
CMSMainSearchFormTest: SilverStripe\CMS\Tests\CMSMainSearchFormTest
ZZZSearchFormTest: SilverStripe\CMS\Tests\ZZZSearchFormTest
MigrateSiteTreeLinkingTaskTest: SilverStripe\CMS\Tests\MigrateSiteTreeLinkingTaskTest
RemoveOrphanedPagesTaskTest: SilverStripe\CMS\Tests\RemoveOrphanedPagesTaskTest
excludedPaths:
- '*/_config/legacy.yml'
warnings:
methods:
'SilverStripe\CMS\Model\SiteTree->getIsAddedToStage()':
message: 'Moved to Versioned->isOnDraftOnly()'
replacement: 'isOnDraftOnly'
'SilverStripe\CMS\Model\SiteTree->getIsModifiedOnStage()':
message: 'Moved to Versioned->isModifiedOnDraft()'
replacement: 'isModifiedOnDraft'
'SilverStripe\CMS\Model\SiteTree->getExistsOnLive()':
message: 'Removed in favour of isPublished()'
replacement: 'isPublished'

View File

@ -1,4 +1,4 @@
Copyright (c) 2016, Silverstripe Ltd.
Copyright (c) 2016, SilverStripe Ltd.
All rights reserved.
Redistribution and use in source and binary forms, with or without

View File

@ -7,7 +7,7 @@ PHP Content Management System (CMS), see [http://silverstripe.org](http://silver
## Installation
See the [getting started documentation](https://docs.silverstripe.org/en/getting_started/).
See [installation instructions](https://docs.silverstripe.org/en/getting_started/installation/).
## Bugtracker

View File

@ -4,6 +4,7 @@ use SilverStripe\Admin\CMSMenu;
use SilverStripe\CMS\Controllers\CMSMain;
use SilverStripe\CMS\Controllers\CMSPageAddController;
use SilverStripe\CMS\Controllers\CMSPageEditController;
use SilverStripe\CMS\Controllers\CMSPageHistoryController;
use SilverStripe\CMS\Controllers\CMSPageSettingsController;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Core\Manifest\ModuleLoader;
@ -32,7 +33,12 @@ ShortcodeParser::get('default')->register(
[SiteTree::class, 'link_shortcode_handler']
);
// TODO Remove once we can configure CMSMenu through static, nested configuration files
CMSMenu::remove_menu_class(CMSMain::class);
CMSMenu::remove_menu_class(CMSPageEditController::class);
CMSMenu::remove_menu_class(CMSPageSettingsController::class);
if (class_exists(CMSPageHistoryController::class)) {
// this class will be removed in CMS 5
CMSMenu::remove_menu_class(CMSPageHistoryController::class);
}
CMSMenu::remove_menu_class(CMSPageAddController::class);

8
_config/legacy.yml Normal file
View File

@ -0,0 +1,8 @@
---
Name: cmslegacy
---
SilverStripe\ORM\DatabaseAdmin:
classname_value_remapping:
SiteTree: 'SilverStripe\CMS\Model\SiteTree'
RedirectorPage: SilverStripe\CMS\Model\RedirectorPage
VirtualPage: SilverStripe\CMS\Model\VirtualPage

View File

@ -5,6 +5,8 @@ After: '#coreroutes'
SilverStripe\Control\Director:
rules:
'': 'SilverStripe\CMS\Controllers\RootURLController'
'RemoveOrphanedPagesTask//$Action/$ID/$OtherID': 'SilverStripe\CMS\Tasks\RemoveOrphanedPagesTask'
'SiteTreeMaintenanceTask//$Action/$ID/$OtherID': 'SilverStripe\CMS\Tasks\SiteTreeMaintenanceTask'
---
Name: legacycmsroutes
---

View File

@ -1,6 +1,6 @@
---
Name: cmsversion
After: 'framework/coreconfig'
After: 'framework/*'
---
SilverStripe\Core\Manifest\VersionProvider:
modules:

View File

@ -0,0 +1,32 @@
<?php
namespace SilverStripe\CMS\GraphQL;
use GraphQL\Type\Definition\ResolveInfo;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\GraphQL\Scaffolding\Scaffolders\CRUD\ReadOne;
use SilverStripe\GraphQL\Scaffolding\StaticSchema;
use SilverStripe\ORM\DataList;
if (!class_exists(ReadOne::class)) {
return;
}
/**
* Shim to make readOnePage work like GraphQL 4
*
* @internal Use GraphQL v4
* @deprecated 4.8..5.0 Use silverstripe/graphql:^4 functionality.
*/
class ReadOneResolver
{
public static function resolve($obj, array $args, array $context, ResolveInfo $info)
{
$idKey = StaticSchema::inst()->formatField('ID');
$id = $args['filter'][$idKey]['eq'];
$readOne = Injector::inst()->createWithArgs(ReadOne::class, ['Page']);
unset($args['filter']);
$args[$idKey] = $id;
return $readOne->resolve($obj, $args, $context, $info);
}
}

View File

@ -1,6 +0,0 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}

View File

@ -1 +1 @@
!function(){"use strict";!function(){function e(e){const t=getComputedStyle(e).display;e.style.display="none",e.dataset.__toggle_display=t}const t=document.querySelectorAll("#switchView a.newWindow");if(t.length>0)for(const e of t.values())e.addEventListener("click",(function(t){t.preventDefault();var n;return window.open(e.href,(n=e.target,document.getElementsByTagName("base")[0].href.replace("http://","").replace(/\//g,"_").replace(/\./g,"_")+n)).focus(),!1}));const n=document.getElementById("SilverStripeNavigatorLinkPopup");if(n){const t=document.getElementById("SilverStripeNavigatorLink");t&&t.addEventListener("click",(function(t){var o;return t.preventDefault(),o=n,"none"!==getComputedStyle(o).display?e(o):function(e){e.style.display=e.dataset.__toggle_display?e.dataset.__toggle_display:"block"}(o),!1}));const o=n.querySelectorAll("a.close");if(o.length>0)for(const t of o.values())t.addEventListener("click",(function(t){return t.preventDefault(),e(n),!1}));const l=n.querySelectorAll("input");if(l.length>0)for(const e of l.values())e.addEventListener("focus",(function(t){e.select()}))}}()}();
!function(e){function t(n){if(r[n])return r[n].exports;var a=r[n]={i:n,l:!1,exports:{}};return e[n].call(a.exports,a,a.exports,t),a.l=!0,a.exports}var r={};t.m=e,t.c=r,t.i=function(e){return e},t.d=function(e,r,n){t.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:n})},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s="./client/src/legacy/SilverStripeNavigator.js")}({"./client/src/legacy/SilverStripeNavigator.js":function(e,t,r){"use strict";!function(){function e(e){return document.getElementsByTagName("base")[0].href.replace("http://","").replace(/\//g,"_").replace(/\./g,"_")+e}function t(e){var t=getComputedStyle(e).display;e.style.display="none",e.dataset.__toggle_display=t}function r(e){e.style.display=e.dataset.__toggle_display?e.dataset.__toggle_display:"block"}function n(e){"none"!==getComputedStyle(e).display?t(e):r(e)}var a=document.querySelectorAll("#switchView a.newWindow");if(a.length>0){var i=!0,l=!1,o=void 0;try{for(var u,c=a.values()[Symbol.iterator]();!(i=(u=c.next()).done);i=!0){var f=u.value;!function(t){t.addEventListener("click",function(r){return r.preventDefault(),window.open(t.href,e(t.target)).focus(),!1})}(f)}}catch(e){l=!0,o=e}finally{try{!i&&c.return&&c.return()}finally{if(l)throw o}}}var v=document.getElementById("SilverStripeNavigatorLinkPopup");if(v){var s=document.getElementById("SilverStripeNavigatorLink");s&&s.addEventListener("click",function(e){return e.preventDefault(),n(v),!1});var d=v.querySelectorAll("a.close");if(d.length>0){var y=!0,p=!1,g=void 0;try{for(var S,m=d.values()[Symbol.iterator]();!(y=(S=m.next()).done);y=!0)S.value.addEventListener("click",function(e){return e.preventDefault(),t(v),!1})}catch(e){p=!0,g=e}finally{try{!y&&m.return&&m.return()}finally{if(p)throw g}}}var h=v.querySelectorAll("input");if(h.length>0){var _=!0,w=!1,b=void 0;try{for(var x,E=h.values()[Symbol.iterator]();!(_=(x=E.next()).done);_=!0){var k=x.value;!function(e){e.addEventListener("focus",function(t){e.select()})}(k)}}catch(e){w=!0,b=e}finally{try{!_&&E.return&&E.return()}finally{if(w)throw b}}}}}()}});

View File

@ -1 +1 @@
!function(){"use strict";var e={964:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;t.default={ANCHORSELECTOR_CURRENT_FIELD:"ANCHORSELECTOR_CURRENT_FIELD",ANCHORSELECTOR_UPDATED:"ANCHORSELECTOR_UPDATED",ANCHORSELECTOR_UPDATING:"ANCHORSELECTOR_UPDATING",ANCHORSELECTOR_UPDATE_FAILED:"ANCHORSELECTOR_UPDATE_FAILED"}},447:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.beginUpdating=function(e){return{type:i.default.ANCHORSELECTOR_UPDATING,payload:{pageId:e}}},t.updateFailed=function(e){return{type:i.default.ANCHORSELECTOR_UPDATE_FAILED,payload:{pageId:e}}},t.updated=function(e,t){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];return{type:i.default.ANCHORSELECTOR_UPDATED,payload:{pageId:e,anchors:t,cacheResult:n}}},t.updatedCurrentField=function(e,t,n){return{type:i.default.ANCHORSELECTOR_CURRENT_FIELD,payload:{pageId:e,anchors:t,fieldID:n}}};var r,i=(r=n(964))&&r.__esModule?r:{default:r}},939:function(e){e.exports=ApolloClient},648:function(e){e.exports=Injector},595:function(e){e.exports=InsertLinkModal},363:function(e){e.exports=React},691:function(e){e.exports=ReactDomClient},624:function(e){e.exports=ReactRedux},265:function(e){e.exports=ShortcodeSerialiser},196:function(e){e.exports=TinyMCEActionRegistrar},754:function(e){e.exports=i18n},311:function(e){e.exports=jQuery}},t={};function n(r){var i=t[r];if(void 0!==i)return i.exports;var o=t[r]={exports:{}};return e[r](o,o.exports,n),o.exports}!function(){var e=p(n(754)),t=p(n(196)),r=p(n(363)),i=n(691),o=n(939),a=n(624),d=p(n(311)),l=p(n(265)),s=n(595),u=n(648),c=n(447);function p(e){return e&&e.__esModule?e:{default:e}}const f="sslinkanchor",E={init(n){t.default.addAction("sslink",{text:e.default._t("CMS.LINKLABEL_ANCHOR","Anchor on a page"),onAction:e=>e.execCommand(f),priority:60},n.getParam("editorIdentifier")).addCommandWithUrlTest(f,/^\[sitetree_link.+]#[^#\]]+$/),n.addCommand(f,(()=>{const e=(0,d.default)(`#${n.id}`).entwine("ss"),t=Number((0,d.default)("#Form_EditForm_ID").val()||0),r=(0,d.default)(n.getBody()).find("[id],[name]").toArray().map((e=>e.id||e.name));ss.store.dispatch((0,c.updatedCurrentField)(t,r,n.id)),e.openLinkAnchorDialog()}))}},A="insert-link__dialog-wrapper--anchor",C=(0,u.provideInjector)((0,s.createInsertLinkModal)("SilverStripe\\CMS\\Controllers\\CMSPageEditController","editorAnchorLink"));d.default.entwine("ss",(t=>{t("textarea.htmleditor").entwine({openLinkAnchorDialog(){let e=t(`#${A}`);e.length||(e=t(`<div id="${A}" />`),t("body").append(e)),e.addClass("insert-link__dialog-wrapper"),e.setElement(this),e.open()}}),t(`#${A}`).entwine({ReactRoot:null,renderModal(n){var d=this;const l=ss.store,s=ss.apolloClient,u=this.getOriginalAttributes(),c=this.getRequireLinkText(),p=Number(t("#Form_EditForm_ID").val()||0);let f=this.getReactRoot();f||(f=(0,i.createRoot)(this[0]),this.setReactRoot(f)),f.render(r.default.createElement(o.ApolloProvider,{client:s},r.default.createElement(a.Provider,{store:l},r.default.createElement(C,{isOpen:n,onInsert:function(){return d.handleInsert(...arguments)},onClosed:()=>this.close(),title:e.default._t("CMS.LINK_ANCHOR","Link to an anchor on a page"),bodyClassName:"modal__dialog",className:"insert-link__dialog-wrapper--anchor",fileAttributes:u,identifier:"Admin.InsertLinkAnchorModal",requireLinkText:c,currentPageID:p}))))},buildAttributes(e){return{href:`${l.default.serialise({name:"sitetree_link",properties:{id:e.PageID}},!0)}${e.Anchor&&e.Anchor.length?`#${e.Anchor}`:""}`,target:e.TargetBlank?"_blank":"",title:e.Description}},getOriginalAttributes(){const e=this.getElement().getEditor(),n=t(e.getSelectedNode()),r=(n.attr("href")||"").split("#");if(!r[0])return{};const i=l.default.match("sitetree_link",!1,r[0]);return i?{PageID:i.properties.id?parseInt(i.properties.id,10):0,Anchor:r[1]||"",Description:n.attr("title"),TargetBlank:!!n.attr("target")}:{}}})})),tinymce.PluginManager.add(f,(e=>E.init(e)))}()}();
!function(e){function t(r){if(n[r])return n[r].exports;var i=n[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var n={};t.m=e,t.c=n,t.i=function(e){return e},t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s="./client/src/legacy/TinyMCE_sslink-anchor.js")}({"./client/src/legacy/TinyMCE_sslink-anchor.js":function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(9),l=r(a),c=n(2),s=r(c),u=n(5),d=r(u),p=n(4),f=n(6),A=n(0),E=r(A),_=n(8),h=r(_),C=n(7),g=n(3),R=n("./client/src/state/anchorSelector/AnchorSelectorActions.js"),O={init:function(e){l.default.addAction("sslink",{text:o.default._t("CMS.LINKLABEL_ANCHOR","Anchor on a page"),onclick:function(e){return e.execCommand("sslinkanchor")},priority:60},e.settings.editorIdentifier).addCommandWithUrlTest("sslinkanchor",/^\[sitetree_link.+]#[^#\]]+$/),e.addCommand("sslinkanchor",function(){var t=(0,E.default)("#"+e.id).entwine("ss"),n=Number((0,E.default)("#Form_EditForm_ID").val()||0),r=e.$("[id],[name]",e.getBody()).toArray().map(function(e){return e.id||e.name});ss.store.dispatch((0,R.updatedCurrentField)(n,r,e.id)),t.openLinkAnchorDialog()})}},T="insert-link__dialog-wrapper--anchor",m=(0,g.provideInjector)((0,C.createInsertLinkModal)("SilverStripe\\CMS\\Controllers\\CMSPageEditController","editorAnchorLink"));E.default.entwine("ss",function(e){e("textarea.htmleditor").entwine({openLinkAnchorDialog:function(){var t=e("#"+T);t.length||(t=e('<div id="'+T+'" />'),e("body").append(t)),t.addClass("insert-link__dialog-wrapper"),t.setElement(this),t.open()}}),e("#"+T).entwine({renderModal:function(t){var n=this,r=ss.store,i=ss.apolloClient,a=function(){return n.close()},l=function(){return n.handleInsert.apply(n,arguments)},c=this.getOriginalAttributes(),u=this.getRequireLinkText(),A=Number(e("#Form_EditForm_ID").val()||0);d.default.render(s.default.createElement(p.ApolloProvider,{client:i},s.default.createElement(f.Provider,{store:r},s.default.createElement(m,{isOpen:t,onInsert:l,onClosed:a,title:o.default._t("CMS.LINK_ANCHOR","Link to an anchor on a page"),bodyClassName:"modal__dialog",className:"insert-link__dialog-wrapper--anchor",fileAttributes:c,identifier:"Admin.InsertLinkAnchorModal",requireLinkText:u,currentPageID:A}))),this[0])},buildAttributes:function(e){return{href:h.default.serialise({name:"sitetree_link",properties:{id:e.PageID}},!0)+(e.Anchor&&e.Anchor.length?"#"+e.Anchor:""),target:e.TargetBlank?"_blank":"",title:e.Description}},getOriginalAttributes:function(){var t=this.getElement().getEditor(),n=e(t.getSelectedNode()),r=(n.attr("href")||"").split("#");if(!r[0])return{};var i=h.default.match("sitetree_link",!1,r[0]);return i?{PageID:i.properties.id?parseInt(i.properties.id,10):0,Anchor:r[1]||"",Description:n.attr("title"),TargetBlank:!!n.attr("target")}:{}}})}),tinymce.PluginManager.add("sslinkanchor",function(e){return O.init(e)}),t.default=O},"./client/src/state/anchorSelector/AnchorSelectorActionTypes.js":function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default={ANCHORSELECTOR_CURRENT_FIELD:"ANCHORSELECTOR_CURRENT_FIELD",ANCHORSELECTOR_UPDATED:"ANCHORSELECTOR_UPDATED",ANCHORSELECTOR_UPDATING:"ANCHORSELECTOR_UPDATING",ANCHORSELECTOR_UPDATE_FAILED:"ANCHORSELECTOR_UPDATE_FAILED"}},"./client/src/state/anchorSelector/AnchorSelectorActions.js":function(e,t,n){"use strict";function r(e){return{type:c.default.ANCHORSELECTOR_UPDATING,payload:{pageId:e}}}function i(e,t){var n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];return{type:c.default.ANCHORSELECTOR_UPDATED,payload:{pageId:e,anchors:t,cacheResult:n}}}function o(e,t,n){return{type:c.default.ANCHORSELECTOR_CURRENT_FIELD,payload:{pageId:e,anchors:t,fieldID:n}}}function a(e){return{type:c.default.ANCHORSELECTOR_UPDATE_FAILED,payload:{pageId:e}}}Object.defineProperty(t,"__esModule",{value:!0}),t.beginUpdating=r,t.updated=i,t.updatedCurrentField=o,t.updateFailed=a;var l=n("./client/src/state/anchorSelector/AnchorSelectorActionTypes.js"),c=function(e){return e&&e.__esModule?e:{default:e}}(l)},0:function(e,t){e.exports=jQuery},1:function(e,t){e.exports=i18n},2:function(e,t){e.exports=React},3:function(e,t){e.exports=Injector},4:function(e,t){e.exports=ReactApollo},5:function(e,t){e.exports=ReactDom},6:function(e,t){e.exports=ReactRedux},7:function(e,t){e.exports=InsertLinkModal},8:function(e,t){e.exports=ShortcodeSerialiser},9:function(e,t){e.exports=TinyMCEActionRegistrar}});

View File

@ -1 +1 @@
!function(){"use strict";var e={939:function(e){e.exports=ApolloClient},648:function(e){e.exports=Injector},595:function(e){e.exports=InsertLinkModal},363:function(e){e.exports=React},691:function(e){e.exports=ReactDomClient},624:function(e){e.exports=ReactRedux},265:function(e){e.exports=ShortcodeSerialiser},196:function(e){e.exports=TinyMCEActionRegistrar},754:function(e){e.exports=i18n},311:function(e){e.exports=jQuery}},t={};function n(i){var r=t[i];if(void 0!==r)return r.exports;var o=t[i]={exports:{}};return e[i](o,o.exports,n),o.exports}!function(){var e=u(n(754)),t=u(n(196)),i=u(n(363)),r=n(691),o=n(939),a=n(624),l=u(n(311)),s=u(n(265)),d=n(595),c=n(648);function u(e){return e&&e.__esModule?e:{default:e}}const p="sslinkinternal",f={init(n){t.default.addAction("sslink",{text:e.default._t("CMS.LINKLABEL_PAGE","Page on this site"),onAction:e=>e.execCommand(p),priority:90},n.getParam("editorIdentifier")).addCommandWithUrlTest(p,/^\[sitetree_link.+]$/),n.addCommand(p,(()=>{(0,l.default)(`#${n.id}`).entwine("ss").openLinkInternalDialog()}))}},g="insert-link__dialog-wrapper--internal",h=(0,c.provideInjector)((0,d.createInsertLinkModal)("SilverStripe\\CMS\\Controllers\\CMSPageEditController","editorInternalLink"));l.default.entwine("ss",(t=>{t("textarea.htmleditor").entwine({openLinkInternalDialog(){let e=t(`#${g}`);e.length||(e=t(`<div id="${g}" />`),t("body").append(e)),e.addClass("insert-link__dialog-wrapper"),e.setElement(this),e.open()}}),t(`#${g}`).entwine({ReactRoot:null,renderModal(t){var n=this;const l=ss.store,s=ss.apolloClient,d=this.getOriginalAttributes(),c=this.getRequireLinkText();let u=this.getReactRoot();u||(u=(0,r.createRoot)(this[0]),this.setReactRoot(u)),u.render(i.default.createElement(o.ApolloProvider,{client:s},i.default.createElement(a.Provider,{store:l},i.default.createElement(h,{isOpen:t,onInsert:function(){return n.handleInsert(...arguments)},onClosed:()=>this.close(),title:e.default._t("CMS.LINK_PAGE","Link to a page"),bodyClassName:"modal__dialog",className:"insert-link__dialog-wrapper--internal",fileAttributes:d,identifier:"Admin.InsertLinkInternalModal",requireLinkText:c}))))},buildAttributes(e){return{href:`${s.default.serialise({name:"sitetree_link",properties:{id:e.PageID}},!0)}${e.Anchor&&e.Anchor.length?`#${e.Anchor}`:""}`,target:e.TargetBlank?"_blank":"",title:e.Description}},getOriginalAttributes(){const e=this.getElement().getEditor(),n=t(e.getSelectedNode()),i=(n.attr("href")||"").split("#");if(!i[0])return{};const r=s.default.match("sitetree_link",!1,i[0]);return r?{PageID:r.properties.id?parseInt(r.properties.id,10):0,Anchor:i[1]||"",Description:n.attr("title"),TargetBlank:!!n.attr("target")}:{}}})})),tinymce.PluginManager.add(p,(e=>f.init(e)))}()}();
!function(e){function t(r){if(n[r])return n[r].exports;var i=n[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var n={};t.m=e,t.c=n,t.i=function(e){return e},t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s="./client/src/legacy/TinyMCE_sslink-internal.js")}({"./client/src/legacy/TinyMCE_sslink-internal.js":function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var i=n(1),o=r(i),l=n(9),a=r(l),s=n(2),u=r(s),c=n(5),d=r(c),f=n(4),p=n(6),g=n(0),k=r(g),_=n(8),m=r(_),h=n(7),x=n(3),I={init:function(e){a.default.addAction("sslink",{text:o.default._t("CMS.LINKLABEL_PAGE","Page on this site"),onclick:function(e){return e.execCommand("sslinkinternal")},priority:90},e.settings.editorIdentifier).addCommandWithUrlTest("sslinkinternal",/^\[sitetree_link.+]$/),e.addCommand("sslinkinternal",function(){(0,k.default)("#"+e.id).entwine("ss").openLinkInternalDialog()})}},A="insert-link__dialog-wrapper--internal",C=(0,x.provideInjector)((0,h.createInsertLinkModal)("SilverStripe\\CMS\\Controllers\\CMSPageEditController","editorInternalLink"));k.default.entwine("ss",function(e){e("textarea.htmleditor").entwine({openLinkInternalDialog:function(){var t=e("#"+A);t.length||(t=e('<div id="'+A+'" />'),e("body").append(t)),t.addClass("insert-link__dialog-wrapper"),t.setElement(this),t.open()}}),e("#"+A).entwine({renderModal:function(e){var t=this,n=ss.store,r=ss.apolloClient,i=function(){return t.close()},l=function(){return t.handleInsert.apply(t,arguments)},a=this.getOriginalAttributes(),s=this.getRequireLinkText();d.default.render(u.default.createElement(f.ApolloProvider,{client:r},u.default.createElement(p.Provider,{store:n},u.default.createElement(C,{isOpen:e,onInsert:l,onClosed:i,title:o.default._t("CMS.LINK_PAGE","Link to a page"),bodyClassName:"modal__dialog",className:"insert-link__dialog-wrapper--internal",fileAttributes:a,identifier:"Admin.InsertLinkInternalModal",requireLinkText:s}))),this[0])},buildAttributes:function(e){return{href:m.default.serialise({name:"sitetree_link",properties:{id:e.PageID}},!0)+(e.Anchor&&e.Anchor.length?"#"+e.Anchor:""),target:e.TargetBlank?"_blank":"",title:e.Description}},getOriginalAttributes:function(){var t=this.getElement().getEditor(),n=e(t.getSelectedNode()),r=(n.attr("href")||"").split("#");if(!r[0])return{};var i=m.default.match("sitetree_link",!1,r[0]);return i?{PageID:i.properties.id?parseInt(i.properties.id,10):0,Anchor:r[1]||"",Description:n.attr("title"),TargetBlank:!!n.attr("target")}:{}}})}),tinymce.PluginManager.add("sslinkinternal",function(e){return I.init(e)}),t.default=I},0:function(e,t){e.exports=jQuery},1:function(e,t){e.exports=i18n},2:function(e,t){e.exports=React},3:function(e,t){e.exports=Injector},4:function(e,t){e.exports=ReactApollo},5:function(e,t){e.exports=ReactDom},6:function(e,t){e.exports=ReactRedux},7:function(e,t){e.exports=InsertLinkModal},8:function(e,t){e.exports=ShortcodeSerialiser},9:function(e,t){e.exports=TinyMCEActionRegistrar}});

View File

@ -1,48 +1 @@
!function(){"use strict";var e={274:function(e,t,n){var a=i(n(180)),o=i(n(521));function i(e){return e&&e.__esModule?e:{default:e}}window.document.addEventListener("DOMContentLoaded",(()=>{(0,o.default)(),(0,a.default)()}))},521:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a=s(n(648)),o=s(n(93)),i=s(n(436)),r=s(n(149));function s(e){return e&&e.__esModule?e:{default:e}}t.default=()=>{a.default.component.register("AnchorSelectorField",o.default),a.default.transform("pages-history",(e=>{e.component("HistoryViewer.pages-controller-cms-content",i.default,"PageHistoryViewer")})),a.default.transform("pages-history-revert",(e=>{e.component("HistoryViewerToolbar.VersionedAdmin.HistoryViewer.SiteTree.HistoryViewerVersionDetail",r.default,"PageRevertMutation")}))}},180:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a=r(n(648)),o=n(827),i=r(n(572));function r(e){return e&&e.__esModule?e:{default:e}}t.default=()=>{a.default.reducer.register("cms",(0,o.combineReducers)({anchorSelector:i.default}))}},93:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.ConnectedAnchorSelectorField=t.Component=void 0;var a=C(n(754)),o=C(n(363)),i=C(n(875)),r=n(624),s=n(827),l=n(762),d=C(n(277)),u=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=_(t);if(n&&n.has(e))return n.get(e);var a={__proto__:null},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var i in e)if("default"!==i&&Object.prototype.hasOwnProperty.call(e,i)){var r=o?Object.getOwnPropertyDescriptor(e,i):null;r&&(r.get||r.set)?Object.defineProperty(a,i,r):a[i]=e[i]}return a.default=e,n&&n.set(e,a),a}(n(447)),c=C(n(892)),f=C(n(42)),p=C(n(453)),h=C(n(78)),m=C(n(720)),g=C(n(820)),v=C(n(86));function _(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(_=function(e){return e?n:t})(e)}function C(e){return e&&e.__esModule?e:{default:e}}const b=()=>null;class w extends d.default{constructor(e){super(e),this.handleChange=this.handleChange.bind(this),this.handleLoadingError=this.handleLoadingError.bind(this)}componentDidMount(){this.ensurePagesLoaded()}componentDidUpdate(e){this.props.pageId!==e.pageId&&this.ensurePagesLoaded()}ensurePagesLoaded(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.props;if(e.loadingState===c.default.UPDATING||e.loadingState===c.default.SUCCESS||!e.pageId)return Promise.resolve();let t=[];e.loadingState===c.default.FIELD_ONLY&&(t=this.props.anchors),e.actions.anchorSelector.beginUpdating(e.pageId);const n=e.data.endpoint.replace(/:id/,e.pageId);return(0,i.default)(n,{credentials:"same-origin"}).then((e=>e.json())).then((n=>{const a=[...new Set([...n,...t])];return e.actions.anchorSelector.updated(e.pageId,a),a})).catch((t=>{e.actions.anchorSelector.updateFailed(e.pageId),this.handleLoadingError(t,e)}))}getDropdownOptions(){const e=this.props.anchors.map((e=>({value:e})));return this.props.value&&!this.props.anchors.find((e=>e===this.props.value))&&e.unshift({value:this.props.value}),e}handleChange(e){"function"==typeof this.props.onChange&&this.props.onChange(e?e.value:"")}handleLoadingError(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.props;if(t.onLoadingError===b)throw e;return t.onLoadingError({errors:[{value:e.message,type:"error"}]})}render(){const{extraClass:e,CreatableSelectComponent:t}=this.props,n=(0,g.default)("anchorselectorfield",e),i=this.getDropdownOptions(),r=this.props.value||"",s=a.default._t("CMS.ANCHOR_SELECT_OR_TYPE","Select or enter anchor");return o.default.createElement(h.default,null,o.default.createElement(t,{isSearchable:!0,isClearable:!0,options:i,className:n,name:this.props.name,onChange:this.handleChange,value:{value:r},noOptionsMessage:()=>a.default._t("CMS.ANCHOR_NO_OPTIONS","No options"),placeholder:s,getOptionLabel:e=>{let{value:t}=e;return t},classNamePrefix:"anchorselectorfield"}))}}t.Component=w,w.propTypes={extraClass:v.default.string,id:v.default.string,name:v.default.string.isRequired,onChange:v.default.func,value:v.default.string,attributes:v.default.oneOfType([v.default.object,v.default.array]),pageId:v.default.number,anchors:v.default.array,loadingState:v.default.oneOf(Object.keys(c.default).map((e=>c.default[e]))),onLoadingError:v.default.func,data:v.default.shape({endpoint:v.default.string,targetFieldName:v.default.string})},w.defaultProps={value:"",extraClass:"",onLoadingError:b,attributes:{},CreatableSelectComponent:p.default};const S=t.ConnectedAnchorSelectorField=(0,r.connect)((function(e,t){const n=(0,l.formValueSelector)(t.formid,m.default),a=t&&t.data&&t.data.targetFieldName||"PageID",o=Number(n(e,a)||0);let i=[];const r=o?e.cms.anchorSelector.pages.find((e=>e.id===o)):null;!r||r.loadingState!==c.default.SUCCESS&&r.loadingState!==c.default.DIRTY&&r.loadingState!==c.default.FIELD_ONLY||(i=r.anchors);let s=null;return s=r?r.loadingState:o?c.default.DIRTY:c.default.SUCCESS,{pageId:o,anchors:i,loadingState:s}}),(function(e){return{actions:{anchorSelector:(0,s.bindActionCreators)(u,e)}}}))(w);t.default=(0,f.default)(S)},554:function(e,t,n){var a;((a=n(311))&&a.__esModule?a:{default:a}).default.entwine("ss",(function(e){e(".TreeDropdownField").entwine({OldValue:null}),e("#Form_AddForm_ParentID_Holder .treedropdownfield").entwine({onmatch(){this._super(),e(".cms-add-form").updateTypeList()}}),e(".cms-add-form .parent-mode :input").entwine({onclick:function(e){var t=this.closest("form").find("#Form_AddForm_ParentID_Holder .TreeDropdownField");"top"==this.val()?(t.setOldValue(t.getValue()),t.setValue(0)):(t.setValue(t.getOldValue()||0),t.setOldValue(null)),t.refresh(),t.trigger("change")}}),e(".cms-add-form").entwine({ParentCache:{},onadd:function(){var t=this;this.find("#Form_AddForm_ParentID_Holder .TreeDropdownField").on("change",(function(){t.updateTypeList()})),this.find(".SelectionGroup.parent-mode").on("change",(function(){t.updateTypeList()})),"top"==e(".cms-add-form .parent-mode :input").val()&&this.updateTypeList()},loadCachedChildren:function(e){var t=this.getParentCache();return void 0!==t[e]?t[e]:null},saveCachedChildren:function(e,t){var n=this.getParentCache();n[e]=t,this.setParentCache(n)},updateTypeList:function(){var t=this.data("hints"),n=this.find("#Form_AddForm_ParentID"),a=this.find("input[name=ParentModeField]:checked").val(),o=n.data("metadata"),i="child"===a?n.getValue():null,r=o?o.ClassName:null,s=r&&"child"===a&&i?r:"Root",l=void 0!==t[s]?t[s]:null,d=this,u=l&&void 0!==l.defaultChild?l.defaultChild:null,c=[];if(i){if(this.hasClass("loading"))return;return this.addClass("loading"),null!==(c=this.loadCachedChildren(i))?(this.updateSelectionFilter(c,u),void this.removeClass("loading")):(e.ajax({url:d.data("childfilter"),data:{ParentID:i},success:function(e){d.saveCachedChildren(i,e),d.updateSelectionFilter(e,u)},complete:function(){d.removeClass("loading")}}),!1)}c=l&&void 0!==l.disallowedChildren?l.disallowedChildren:[],this.updateSelectionFilter(c,u)},updateSelectionFilter:function(t,n){var a=this.find("#Form_AddForm_PageType div.radio.selected")[0],o=!1,i=null;if(this.find("#Form_AddForm_PageType div.radio").each((function(n,r){var s=e(this).find("input").val(),l=-1===e.inArray(s,t);r===a&&l&&(o=!0),e(this).setEnabled(l),l||e(this).setSelected(!1),i=(null===i||i)&&l})),o)var r=e(a).parents("li:first");else if(n)r=this.find("#Form_AddForm_PageType div.radio input[value="+n+"]").parents("li:first");else r=this.find("#Form_AddForm_PageType div.radio:not(.disabled):first");r.setSelected(!0),r.siblings().setSelected(!1),this.find("#Form_AddForm_PageType div.radio:not(.disabled)").length?this.find("button[name=action_doAdd]").removeAttr("disabled"):this.find("button[name=action_doAdd]").attr("disabled","disabled"),this.find(".message-restricted")[i?"hide":"show"]()}}),e(".cms-add-form #Form_AddForm_PageType div.radio").entwine({onclick:function(e){this.setSelected(!0)},setSelected:function(e){var t=this.find("input");e&&!t.is(":disabled")?(this.siblings().setSelected(!1),this.toggleClass("selected",!0),t.prop("checked",!0)):(this.toggleClass("selected",!1),t.prop("checked",!1))},setEnabled:function(t){e(this).toggleClass("disabled",!t),t?e(this).find("input").removeAttr("disabled"):e(this).find("input").attr("disabled","disabled").removeAttr("checked")}}),e(".cms-content-addpage-button").entwine({onclick:function(t){var n,a=e(".cms-tree"),o=e(".cms-list"),i=0;if(a.is(":visible")){var r=a.jstree("get_selected");i=r?e(r[0]).data("id"):null}else{var s=o.find('input[name="Page[GridState]"]').val();s&&(i=parseInt(JSON.parse(s).ParentID,10))}var l,d={selector:this.data("targetPanel"),pjax:this.data("pjax")};i?(n=this.data("extraParams")?this.data("extraParams"):"",l=e.path.addSearchParams(i18n.sprintf(this.data("urlAddpage"),i),n)):l=this.attr("href"),e(".cms-container").loadPanel(l,null,d),t.preventDefault(),this.blur()}})}))},649:function(e,t,n){var a=r(n(311)),o=r(n(754)),i=r(n(141));function r(e){return e&&e.__esModule?e:{default:e}}a.default.entwine("ss",(function(e){e(".cms-edit-form :input#Form_EditForm_ClassName").entwine({onchange:function(){alert(o.default._t("CMS.ALERTCLASSNAME"))}}),e(".cms-edit-form input[name=Title]").entwine({onmatch:function(){var t=this;t.data("OrigVal",t.val());var n=t.closest("form"),a=e("input:text[name=URLSegment]",n),o=e("input[name=LiveLink]",n);a.length>0&&(t._addActions(),this.on("change",(function(n){var i=t.data("OrigVal"),r=t.val();t.data("OrigVal",r),0===a.val().indexOf(a.data("defaultUrl"))&&""==o.val()?t.updateURLSegment(r):e(".update",t.parent()).show().parent(".form__field-holder").addClass("input-group"),t.updateRelatedFields(r,i),t.updateBreadcrumbLabel(r)}))),this._super()},onunmatch:function(){this._super()},updateRelatedFields:function(t,n){this.parents("form").find("input[name=MetaTitle], input[name=MenuTitle]").each((function(){var a=e(this);a.val()==n&&(a.val(t),a.updatedRelatedFields&&a.updatedRelatedFields())}))},updateURLSegment:function(t){var n=e("input:text[name=URLSegment]",this.closest("form")).closest(".field.urlsegment"),a=e(".update",this.parent());n.update(t),a.is(":visible")&&a.hide().parent(".form__field-holder").removeClass("input-group")},updateBreadcrumbLabel:function(t){e(".cms-edit-form input[name=ID]").val();var n=e("span.cms-panel-link.crumb");t&&""!=t&&n.text(t)},_addActions:function(){var t,n=this;(t=e("<button />",{class:"update btn btn-outline-secondary form__field-update-url",text:o.default._t("CMS.UpdateURL"),type:"button",click:function(e){e.preventDefault(),n.updateURLSegment(n.val())}})).insertAfter(n),t.hide()}}),e(".cms-edit-form .parentTypeSelector").entwine({onmatch:function(){var e=this;this.find(":input[name=ParentType]").on("click",(function(t){e._toggleSelection(t)})),this.find(".TreeDropdownField").on("change",(function(t){e._changeParentId(t)})),this._changeParentId(),this._toggleSelection(),this._super()},onunmatch:function(){this._super()},_toggleSelection:function(t){var n=this.find(":input[name=ParentType]:checked").val(),a=this.find("#Form_EditForm_ParentID_Holder");"root"==n?this.find(":input[name=ParentID]").val(0):this.find(":input[name=ParentID]").val(this.find("#Form_EditForm_ParentType_subpage").data("parentIdValue")),"root"!=n?a.slideDown(400,(function(){e(this).css("overflow","visible")})):a.slideUp()},_changeParentId:function(e){var t=this.find(":input[name=ParentID]").val();this.find("#Form_EditForm_ParentType_subpage").data("parentIdValue",t)}}),e(".cms-edit-form .btn-toolbar #Form_EditForm_action_doRollback, .cms-edit-form .btn-toolbar #Form_EditForm_action_rollback").entwine({onclick:function(e){if(this.is(":disabled"))return e.preventDefault(),!1;const t=this.parents("form:first").find(":input[name=Version]").val(),n=t?o.default.sprintf(o.default._t("CMS.RollbackToVersion","Do you really want to roll back to version #%s of this page?"),t):o.default._t("CMS.ConfirmRestoreFromLive","Are you sure you want to revert draft to when the page was last published?");return confirm(n)?(this.parents("form:first").addClass("loading"),this._super(e)):(e.preventDefault(),!1)}}),e(".cms-edit-form .btn-toolbar #Form_EditForm_action_archive:not(.homepage-warning)").entwine({onclick:function(e){var t;return t=this.parents("form:first").find("input[name=ArchiveWarningMessage]").val().replace(/\\n/g,"\n"),!!confirm(t)&&(this.parents("form:first").addClass("loading"),this._super(e))}}),e(".cms-edit-form .btn-toolbar #Form_EditForm_action_restore").entwine({onclick:function(e){var t,n=this.parents("form:first").find(":input[name=Version]").val(),a=this.data("toRoot");return t=o.default.sprintf(o.default._t(a?"CMS.RestoreToRoot":"CMS.Restore"),n),!!confirm(t)&&(this.parents("form:first").addClass("loading"),this._super(e))}}),e(".cms-edit-form .btn-toolbar #Form_EditForm_action_unpublish:not(.homepage-warning)").entwine({onclick:function(e){var t,n=this.parents("form:first").find(":input[name=Version]").val();return t=o.default.sprintf(o.default._t("CMS.Unpublish"),n),!!confirm(t)&&(this.parents("form:first").addClass("loading"),this._super(e))}}),e(".cms-edit-form.changed").entwine({onmatch:function(t){this.find("button[data-text-alternate]").each((function(){const t=e(this),n=t.find(".btn__title"),a=t.data("textAlternate");a&&(t.data("textStandard",n.text()),n.text(a));const o=t.data("btnAlternate");o&&(t.data("btnStandard",t.attr("class")),t.attr("class",o),t.removeClass("btn-outline-secondary").addClass("btn-primary"));const i=t.data("btnAlternateAdd");i&&t.addClass(i);const r=t.data("btnAlternateRemove");r&&t.removeClass(r)})),this._super(t)},onunmatch:function(t){this.find("button[data-text-alternate]").each((function(){const t=e(this),n=t.find(".btn__title"),a=t.data("textStandard");a&&n.text(a);const o=t.data("btnStandard");o&&(t.attr("class",o),t.addClass("btn-outline-secondary").removeClass("btn-primary"));const i=t.data("btnAlternateAdd");i&&t.removeClass(i);const r=t.data("btnAlternateRemove");r&&t.addClass(r)})),this._super(t)}}),e(".cms-edit-form .btn-toolbar button[name=action_publish]").entwine({onbuttonafterrefreshalternate:function(){this.data("showingAlternate")?(this.addClass("btn-primary"),this.removeClass("btn-secondary")):(this.removeClass("btn-primary"),this.addClass("btn-secondary"))}}),e(".cms-edit-form .btn-toolbar button[name=action_save]").entwine({onbuttonafterrefreshalternate:function(){this.data("showingAlternate")?(this.addClass("btn-primary"),this.removeClass("btn-secondary")):(this.removeClass("btn-primary"),this.addClass("btn-secondary"))}}),e('.cms-edit-form.CMSPageSettingsController input[name="ParentType"]:checked').entwine({onmatch:function(){this.redraw(),this._super()},onunmatch:function(){this._super()},redraw:function(){var t=e(".cms-edit-form.CMSPageSettingsController #Form_EditForm_ParentID_Holder");"Form_EditForm_ParentType_root"==e(this).attr("id")?t.slideUp():t.slideDown()},onclick:function(){this.redraw()}}),"Form_EditForm_ParentType_root"==e('.cms-edit-form.CMSPageSettingsController input[name="ParentType"]:checked').attr("id")&&e(".cms-edit-form.CMSPageSettingsController #Form_EditForm_ParentID_Holder").hide();var t=!1;e(".cms-edit-form .btn-toolbar #Form_EditForm_action_unpublish.homepage-warning,.cms-edit-form .btn-toolbar #Form_EditForm_action_archive.homepage-warning,#Form_EditForm_URLSegment_Holder.homepage-warning .btn.update").entwine({onclick:async function(e){if(t)return this._super(e);e.stopPropagation();var n=o.default._t("CMS.RemoveHomePageWarningMessage","Warning: This page is the home page. By changing the URL segment visitors will not be able to view it.");return await(0,i.default)({title:o.default._t("CMS.RemoveHomePageWarningTitle","Remove your home page?"),message:n,confirmText:o.default._t("CMS.RemoveHomePageWarningLabel","Remove"),confirmColor:"danger"})&&(t=!0,this.trigger("click"),t=!1),!1}})}))},978:function(e,t,n){var a=s(n(311)),o=s(n(754)),i=s(n(141)),r=n(845);function s(e){return e&&e.__esModule?e:{default:e}}a.default.entwine("ss.tree",(function(e){e(".cms-tree").entwine({fromDocument:{"oncontext_show.vakata":function(e){this.adjustContextClass()}},adjustContextClass:function(){var t=e("#vakata-contextmenu").find("ul ul");t.each((function(n){var a="1",o=e(t[n]).find("li").length;o>20?a="3":o>10&&(a="2"),e(t[n]).addClass("vakata-col-"+a).removeClass("right"),e(t[n]).find("li").on("mouseenter",(function(t){e(this).parent("ul").removeClass("right")}))}))},showListViewFor:function(t){localStorage.setItem("ss.pages-view-type","listview");const n=this.closest(".cms-content-view").data("url-listviewroot"),a=e.path.addSearchParams(n,{ParentID:t}),o=e("base").attr("href")||"";window.location.assign((0,r.joinUrlPaths)(o,a))},getTreeConfig:function(){var t=this,n=this._super();this.getHints();return n.plugins.push("contextmenu"),n.contextmenu={items:function(n){var a={edit:{label:n.hasClass("edit-disabled")?o.default._t("CMS.EditPage","Edit page",100,"Used in the context menu when right-clicking on a page node in the CMS tree"):o.default._t("CMS.ViewPage","View page",100,"Used in the context menu when right-clicking on a page node in the CMS tree"),action:function(n){e(".cms-container").entwine(".ss").loadPanel(o.default.sprintf(t.data("urlEditpage"),n.data("id")))}}};n.hasClass("nochildren")||(a.showaslist={label:o.default._t("CMS.ShowAsList"),action:function(e){t.showListViewFor(e.data("id"))}});n.data("pagetype");var i=n.data("id"),r=n.find(">a .item").data("allowedchildren"),s={},l=!1;return e.each(r,(function(n,a){l=!0,s["allowedchildren-"+a.ClassName]={label:'<span class="jstree-pageicon '+a.IconClass+'"></span>'+a.Title,_class:"class-"+a.ClassName.replace(/[^a-zA-Z0-9\-_:.]+/g,"_"),action:function(n){e(".cms-container").entwine(".ss").loadPanel(e.path.addSearchParams(o.default.sprintf(t.data("urlAddpage"),i,a.ClassName),t.data("extraParams")))}}})),l&&(a.addsubpage={label:o.default._t("CMS.AddSubPage","Add page under this page",100,"Used in the context menu when right-clicking on a page node in the CMS tree"),submenu:s}),n.hasClass("edit-disabled")||(a.duplicate={label:o.default._t("CMS.Duplicate"),submenu:[{label:o.default._t("CMS.ThisPageOnly"),action:function(n){e(".cms-container").entwine(".ss").loadPanel(e.path.addSearchParams(o.default.sprintf(t.data("urlDuplicate"),n.data("id")),t.data("extraParams")))}},{label:o.default._t("CMS.ThisPageAndSubpages"),action:function(n){e(".cms-container").entwine(".ss").loadPanel(e.path.addSearchParams(o.default.sprintf(t.data("urlDuplicatewithchildren"),n.data("id")),t.data("extraParams")))}}]}),a}},n},canMove:async function(e){if(!(e.rslt.o.find(".homepage").first().length>0))return!0;if(e.rslt.op.data("id")===e.rslt.np.data("id"))return!0;var t=o.default._t("CMS.RemoveHomePageWarningMessage","Warning: This page is the home page. By changing the URL segment visitors will not be able to view it.");return await(0,i.default)({title:o.default._t("CMS.RemoveHomePageWarningTitle","Remove your home page?"),message:t,confirmText:o.default._t("CMS.RemoveHomePageWarningLabel","Remove"),confirmColor:"danger"})}}),e(".cms-tree a.jstree-clicked").entwine({onmatch:function(){var e=this,t=e.parents(".cms-tree-view-sidebar");if(e.offset().top<0||e.offset().top>t.height()-e.height()){var n=e.parent();n.prev().length&&(n=n.prev()),n.get(0).scrollIntoView()}}}),e(".cms-tree-filtered .clear-filter").entwine({onclick:function(){window.location=location.protocol+"//"+location.host+location.pathname}}),e(".cms-tree .subtree-list-link").entwine({onclick:function(e){e.preventDefault(),this.closest(".cms-tree").showListViewFor(this.data("id"))}})}))},580:function(e,t,n){var a,o=(a=n(311))&&a.__esModule?a:{default:a},i=n(845);o.default.entwine("ss",(function(e){const t="treeview",n="listview";e(".cms-content-header-info").entwine({"from .cms-panel":{ontoggle:function(e){var t=this.closest(".cms-content").find(e.target);0!==t.length&&this.parent()[t.hasClass("collapsed")?"addClass":"removeClass"]("collapsed")}}}),e(".cms-panel-deferred.cms-content-view").entwine({onadd:function(){if(this.data("no-ajax"))return;var e=localStorage.getItem("ss.pages-view-type")||t;this.closest(".cms-content-tools").length>0&&(e=t);const a=this.data(`url-${e}`);let o=localStorage.getItem("ss.pages-view-filtered");"string"==typeof o&&"false"===o.toLowerCase()&&(o=!1),localStorage.setItem("ss.pages-view-filtered",!1),this.data("deferredNoCache",o||e===n),this.data("url",a+location.search),this._super()}}),e(".js-injector-boot .search-holder--cms").entwine({search(e){localStorage.setItem("ss.pages-view-filtered",!0),this._super(e)}}),e(".cms .page-view-link").entwine({onclick:function(t){t.preventDefault();const a=e(this).data("view"),o=this.closest(".cms-content-view"),r=o.data(`url-${a}`),s=0!==o.closest(".cms-content-tools").length;if(localStorage.setItem("ss.pages-view-type",a),s&&a===n){const t=e("base").attr("href")||"";window.location.assign((0,i.joinUrlPaths)(t,o.data("url-listviewroot")))}else o.data("url",r+location.search),o.redraw()}}),e(".cms .cms-clear-filter").entwine({onclick:function(t){t.preventDefault(),window.location=e(this).prop("href")}}),e(".cms-content-toolbar").entwine({onmatch:function(){var t=this;this._super(),e.each(this.find(".cms-actions-buttons-row .tool-button"),(function(){var n=e(this),a=n.data("toolid");n.hasClass("active");void 0!==a&&(n.data("active",!1).removeClass("active"),e("#"+a).hide(),t.bindActionButtonEvents(n))}))},onunmatch:function(){var t=this;this._super(),e.each(this.find(".cms-actions-buttons-row .tool-button"),(function(){var n=e(this);t.unbindActionButtonEvents(n)}))},bindActionButtonEvents:function(e){var t=this;e.on("click.cmsContentToolbar",(function(n){t.showHideTool(e)}))},unbindActionButtonEvents:function(e){e.off(".cmsContentToolbar")},showHideTool:function(t){var n=t.data("active"),a=t.data("toolid"),o=e("#"+a);e.each(this.find(".cms-actions-buttons-row .tool-button"),(function(){var t=e(this),n=e("#"+t.data("toolid"));t.data("toolid")!==a&&(n.hide(),t.data("active",!1))})),t[n?"removeClass":"addClass"]("active"),o[n?"hide":"show"](),t.data("active",!n)}})}))},907:function(e,t,n){var a;((a=n(311))&&a.__esModule?a:{default:a}).default.entwine("ss",(function(e){e("#Form_EditForm_RedirectionType input").entwine({onmatch:function(){e(this).attr("checked")&&this.toggle(),this._super()},onunmatch:function(){this._super()},onclick:function(){this.toggle()},toggle:function(){"Internal"==e(this).attr("value")?(e("#Form_EditForm_ExternalURL_Holder").hide(),e("#Form_EditForm_LinkToID_Holder").show(),e("#Form_EditForm_LinkToFile_Holder").hide()):"External"==e(this).attr("value")?(e("#Form_EditForm_ExternalURL_Holder").show(),e("#Form_EditForm_LinkToID_Holder").hide(),e("#Form_EditForm_LinkToFile_Holder").hide()):(e("#Form_EditForm_LinkToFile_Holder").show(),e("#Form_EditForm_ExternalURL_Holder").hide(),e("#Form_EditForm_LinkToID_Holder").hide())}})}))},806:function(e,t,n){var a;((a=n(311))&&a.__esModule?a:{default:a}).default.entwine("ss",(function(e){e(".field.urlsegment:not(.readonly)").entwine({MaxPreviewLength:55,Ellipsis:"...",onmatch:function(){this.find(":text").length&&this.toggleEdit(!1),this.redraw(),this._super()},redraw:function(){var e=this.find(":text"),t=decodeURI(e.data("prefix")+e.val()),n=t;t.length>this.getMaxPreviewLength()&&(n=this.getEllipsis()+t.substr(t.length-this.getMaxPreviewLength(),t.length)),this.find(".URL-link").attr("href",encodeURI(t+e.data("suffix"))).text(n)},toggleEdit:function(e){var t=this.find(":text");this.find(".preview-holder")[e?"hide":"show"](),this.find(".edit-holder")[e?"show":"hide"](),e&&(t.data("origval",t.val()),t.focus())},update:function(){var e=this,t=this.find(":text"),n=t.data("origval"),a=arguments[0],o=a&&""!==a?a:t.val();n!=o?(this.addClass("loading"),this.suggest(o,(function(n){t.val(decodeURIComponent(n.value)),e.toggleEdit(!1),e.removeClass("loading"),e.redraw()}))):(this.toggleEdit(!1),this.redraw())},cancel:function(){var e=this.find(":text");e.val(e.data("origval")),this.toggleEdit(!1)},suggest:function(t,n){var a=this,o=a.find(":text"),i=e.path.parseUrl(a.closest("form").attr("action")),r=i.hrefNoSearch+"/field/"+o.attr("name")+"/suggest/?value="+encodeURIComponent(t);i.search&&(r+="&"+i.search.replace(/^\?/,"")),e.ajax({url:r,success:function(e){n.apply(this,arguments)},error:function(e,t){e.statusText=e.responseText},complete:function(){a.removeClass("loading")}})}}),e(".field.urlsegment .text").entwine({onkeydown:function(e){13===e.keyCode&&(e.preventDefault(),this.closest(".field").update())}}),e(".field.urlsegment .edit").entwine({onclick:function(e){e.preventDefault(),this.closest(".field").toggleEdit(!0)}}),e(".field.urlsegment .update").entwine({onclick:function(e){e.preventDefault(),this.closest(".field").update()}}),e(".field.urlsegment .cancel").entwine({onclick:function(e){e.preventDefault(),this.closest(".field").cancel()}})}))},964:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;t.default={ANCHORSELECTOR_CURRENT_FIELD:"ANCHORSELECTOR_CURRENT_FIELD",ANCHORSELECTOR_UPDATED:"ANCHORSELECTOR_UPDATED",ANCHORSELECTOR_UPDATING:"ANCHORSELECTOR_UPDATING",ANCHORSELECTOR_UPDATE_FAILED:"ANCHORSELECTOR_UPDATE_FAILED"}},447:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.beginUpdating=function(e){return{type:o.default.ANCHORSELECTOR_UPDATING,payload:{pageId:e}}},t.updateFailed=function(e){return{type:o.default.ANCHORSELECTOR_UPDATE_FAILED,payload:{pageId:e}}},t.updated=function(e,t){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];return{type:o.default.ANCHORSELECTOR_UPDATED,payload:{pageId:e,anchors:t,cacheResult:n}}},t.updatedCurrentField=function(e,t,n){return{type:o.default.ANCHORSELECTOR_CURRENT_FIELD,payload:{pageId:e,anchors:t,fieldID:n}}};var a,o=(a=n(964))&&a.__esModule?a:{default:a}},572:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:s,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;const n=(n,o)=>{const i=t.payload.pageId;return(0,a.default)({pages:[...e.pages.filter((e=>e.id!==i)),{id:i,loadingState:n,anchors:o}].sort(((e,t)=>e.id-t.id))})};switch(t.type){case o.default.ANCHORSELECTOR_UPDATING:return n(i.default.UPDATING,[]);case o.default.ANCHORSELECTOR_UPDATED:{const{anchors:e,cacheResult:a}=t.payload,{SUCCESS:o,DIRTY:r}=i.default;return n(a?o:r,e)}case o.default.ANCHORSELECTOR_CURRENT_FIELD:{const{anchors:e}=t.payload;return n(i.default.FIELD_ONLY,e)}case o.default.ANCHORSELECTOR_UPDATE_FAILED:return n(i.default.FAILED,[]);default:return e}};var a=r(n(752)),o=r(n(964)),i=r(n(892));function r(e){return e&&e.__esModule?e:{default:e}}const s=(0,a.default)({pages:[]})},892:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;t.default={SUCCESS:"SUCCESS",DIRTY:"DIRTY",FIELD_ONLY:"FIELD_ONLY",UPDATING:"UPDATING",FAILED:"FAILED"}},436:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.query=t.default=t.config=void 0;var a,o=n(732),i=(a=n(306))&&a.__esModule?a:{default:a};const r=t.query=i.default`
query ReadHistoryViewerPage ($page_id: ID!, $limit: Int!, $offset: Int!) {
readOnePage(
versioning: {
mode: ALL_VERSIONS
},
filter: {
id: { eq: $page_id }
}
) {
id
versions (limit: $limit, offset: $offset, sort: {
version: DESC
}) {
pageInfo {
totalCount
}
nodes {
version
absoluteLink
author {
firstName
surname
}
publisher {
firstName
surname
}
deleted
draft
published
liveVersion
latestDraftVersion
lastEdited
}
}
}
}
`,s=t.config={options(e){let{recordId:t,limit:n,page:a}=e;return{variables:{limit:n,offset:((a||1)-1)*n,page_id:t},fetchPolicy:"network-only"}},props(e){let{data:{error:t,refetch:n,readOnePage:a,loading:o},ownProps:{actions:i={versions:{}},limit:r,recordId:s}}=e;const l=a||null;return{loading:o||!l,versions:l,graphQLErrors:t&&t.graphQLErrors&&t.graphQLErrors.map((e=>e.message)),actions:{...i,versions:{...l,goToPage(e){n({offset:((e||1)-1)*r,limit:r,page_id:s})}}}}}};t.default=(0,o.graphql)(r,s)},149:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.mutation=t.default=t.config=void 0;var a,o=n(732),i=(a=n(306))&&a.__esModule?a:{default:a};const r=t.mutation=i.default`
mutation rollbackPage($id:ID!, $toVersion:Int!) {
rollbackPage(
id: $id
toVersion: $toVersion
) {
id
}
}
`,s=t.config={props:e=>{let{mutate:t,ownProps:{actions:n}}=e;const a=(e,n)=>t({variables:{id:e,toVersion:n}});return{actions:{...n,rollbackPage:a,revertToVersion:a}}},options:{refetchQueries:["ReadHistoryViewerPage"]}};t.default=(0,o.graphql)(r,s)},112:function(e,t,n){function a(e){return a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},a(e)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var o,i=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!==a(e)&&"function"!=typeof e)return{default:e};var n=l(t);if(n&&n.has(e))return n.get(e);var o={},i=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var r in e)if("default"!==r&&Object.prototype.hasOwnProperty.call(e,r)){var s=i?Object.getOwnPropertyDescriptor(e,r):null;s&&(s.get||s.set)?Object.defineProperty(o,r,s):o[r]=e[r]}o.default=e,n&&n.set(e,o);return o}(n(363)),r=(o=n(86))&&o.__esModule?o:{default:o},s=n(127);function l(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(l=function(e){return e?n:t})(e)}function d(){return d=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var a in n)Object.prototype.hasOwnProperty.call(n,a)&&(e[a]=n[a])}return e},d.apply(this,arguments)}var u=function(e){var t=e.onClose,n=e.message,a=e.title,o=e.confirmText,r=e.cancelText,l=e.confirmColor,u=e.cancelColor,c=e.className,f=e.buttonsComponent,p=e.size,h=e.bodyComponent,m=e.modalProps,g=i.default.createElement(i.Fragment,null,r&&i.default.createElement(s.Button,{color:u,onClick:function(){return t(!1)}},r)," ",i.default.createElement(s.Button,{color:l,onClick:function(){return t(!0)}},o));if(f){var v=f;g=i.default.createElement(v,{onClose:t})}var _=h;return i.default.createElement(s.Modal,d({size:p,isOpen:!0,toggle:function(){return t(!1)},className:"reactstrap-confirm ".concat(c)},m),a&&i.default.createElement(s.ModalHeader,{toggle:function(){return t(!1)}},a||null),i.default.createElement(s.ModalBody,null,h?i.default.createElement(_,null):n),i.default.createElement(s.ModalFooter,null,g))};u.defaultProps={message:"Are you sure?",title:"Warning!",confirmText:"Ok",cancelText:"Cancel",confirmColor:"primary",cancelColor:"",className:"",buttonsComponent:null,size:null,bodyComponent:null,modalProps:{}},u.propTypes={onClose:r.default.func.isRequired,message:r.default.node,title:r.default.node,confirmText:r.default.node,cancelText:r.default.node,confirmColor:r.default.string,cancelColor:r.default.string,className:r.default.string,size:r.default.string,buttonsComponent:r.default.func,bodyComponent:r.default.func,modalProps:r.default.object};var c=u;t.default=c},141:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a=r(n(363)),o=n(394),i=r(n(112));function r(e){return e&&e.__esModule?e:{default:e}}function s(){return s=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var a in n)Object.prototype.hasOwnProperty.call(n,a)&&(e[a]=n[a])}return e},s.apply(this,arguments)}var l=function(e){return new Promise((function(t){var n=document.createElement("div");(0,o.render)(a.default.createElement(i.default,s({},e,{onClose:function(e){(0,o.unmountComponentAtNode)(n),n=null,t(e)}})),n)}))};t.default=l},732:function(e){e.exports=ApolloClientReactHoc},752:function(e){e.exports=DeepFreezeStrict},78:function(e){e.exports=EmotionCssCacheProvider},42:function(e){e.exports=FieldHolder},306:function(e){e.exports=GraphQLTag},648:function(e){e.exports=Injector},875:function(e){e.exports=IsomorphicFetch},86:function(e){e.exports=PropTypes},363:function(e){e.exports=React},394:function(e){e.exports=ReactDom},624:function(e){e.exports=ReactRedux},453:function(e){e.exports=ReactSelectCreatable},127:function(e){e.exports=Reactstrap},827:function(e){e.exports=Redux},762:function(e){e.exports=ReduxForm},277:function(e){e.exports=SilverStripeComponent},820:function(e){e.exports=classnames},720:function(e){e.exports=getFormState},754:function(e){e.exports=i18n},311:function(e){e.exports=jQuery},845:function(e){e.exports=ssUrlLib}},t={};function n(a){var o=t[a];if(void 0!==o)return o.exports;var i=t[a]={exports:{}};return e[a](i,i.exports,n),i.exports}n(554),n(649),n(580),n(978),n(907),n(806),n(274)}();
!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={i:r,l:!1,exports:{}};return e[r].call(a.exports,a,a.exports,t),a.l=!0,a.exports}var n={};t.m=e,t.c=n,t.i=function(e){return e},t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s="./client/src/bundles/bundle.js")}({"./client/src/boot/index.js":function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}var a=n("./client/src/boot/registerReducers.js"),o=r(a),i=n("./client/src/boot/registerComponents.js"),s=r(i);window.document.addEventListener("DOMContentLoaded",function(){(0,s.default)(),(0,o.default)()})},"./client/src/boot/registerComponents.js":function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(3),o=r(a),i=n("./client/src/components/AnchorSelectorField/AnchorSelectorField.js"),s=r(i),l=n("./client/src/state/history/readOnePageQuery.js"),d=r(l),c=n("./client/src/state/history/rollbackPageMutation.js"),u=r(c);t.default=function(){o.default.component.register("AnchorSelectorField",s.default),o.default.transform("pages-history",function(e){e.component("HistoryViewer.pages-controller-cms-content",d.default,"PageHistoryViewer")}),o.default.transform("pages-history-revert",function(e){e.component("HistoryViewerToolbar.VersionedAdmin.HistoryViewer.SiteTree.HistoryViewerVersionDetail",u.default,"PageRevertMutation")})}},"./client/src/boot/registerReducers.js":function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(3),o=r(a),i=n(12),s=n("./client/src/state/anchorSelector/AnchorSelectorReducer.js"),l=r(s);t.default=function(){o.default.reducer.register("cms",(0,i.combineReducers)({anchorSelector:l.default}))}},"./client/src/bundles/bundle.js":function(e,t,n){"use strict";n("./client/src/legacy/CMSMain.AddForm.js"),n("./client/src/legacy/CMSMain.EditForm.js"),n("./client/src/legacy/CMSMain.js"),n("./client/src/legacy/CMSMain.Tree.js"),n("./client/src/legacy/CMSPageHistoryController.js"),n("./client/src/legacy/RedirectorPage.js"),n("./client/src/legacy/SiteTreeURLSegmentField.js"),n("./client/src/boot/index.js")},"./client/src/components/AnchorSelectorField/AnchorSelectorField.js":function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t<e.length;t++)n[t]=e[t];return n}return Array.from(e)}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function l(e,t){var n=(0,b.formValueSelector)(t.formid,O.default),r=t&&t.data&&t.data.targetFieldName||"PageID",a=Number(n(e,r)||0),o=[],i=a?e.cms.anchorSelector.pages.find(function(e){return e.id===a}):null;!i||i.loadingState!==E.default.SUCCESS&&i.loadingState!==E.default.DIRTY&&i.loadingState!==E.default.FIELD_ONLY||(o=i.anchors);var s=null;return s=i?i.loadingState:a?E.default.DIRTY:E.default.SUCCESS,{pageId:a,anchors:o,loadingState:s}}function d(e){return{actions:{anchorSelector:(0,_.bindActionCreators)(w,e)}}}Object.defineProperty(t,"__esModule",{value:!0}),t.ConnectedAnchorSelectorField=t.Component=void 0;var c=function(){function e(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}return function(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}}(),u=n(1),f=r(u),p=n(2),h=r(p),m=n(15),g=r(m),v=n(6),_=n(12),b=n(18),C=n(19),y=r(C),S=n("./client/src/state/anchorSelector/AnchorSelectorActions.js"),w=function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t.default=e,t}(S),P=n("./client/src/state/anchorSelector/AnchorSelectorStates.js"),E=r(P),x=n(14),F=r(x),R=n(16),A=n(21),O=r(A),T=n(20),j=r(T),L=n(11),M=r(L),D=function(){return null},I=function(e){function t(e){o(this,t);var n=i(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return n.handleChange=n.handleChange.bind(n),n.handleLoadingError=n.handleLoadingError.bind(n),n}return s(t,e),c(t,[{key:"componentDidMount",value:function(){this.ensurePagesLoaded()}},{key:"componentDidUpdate",value:function(e){this.props.pageId!==e.pageId&&this.ensurePagesLoaded()}},{key:"ensurePagesLoaded",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.props;if(t.loadingState===E.default.UPDATING||t.loadingState===E.default.SUCCESS||!t.pageId)return Promise.resolve();var n=[];t.loadingState===E.default.FIELD_ONLY&&(n=this.props.anchors),t.actions.anchorSelector.beginUpdating(t.pageId);var r=t.data.endpoint.replace(/:id/,t.pageId);return(0,g.default)(r,{credentials:"same-origin"}).then(function(e){return e.json()}).then(function(e){var r=[].concat(a(new Set([].concat(a(e),a(n)))));return t.actions.anchorSelector.updated(t.pageId,r),r}).catch(function(n){t.actions.anchorSelector.updateFailed(t.pageId),e.handleLoadingError(n,t)})}},{key:"getDropdownOptions",value:function(){var e=this,t=this.props.anchors.map(function(e){return{value:e}});return this.props.value&&!this.props.anchors.find(function(t){return t===e.props.value})&&t.unshift({value:this.props.value}),t}},{key:"handleChange",value:function(e){"function"==typeof this.props.onChange&&this.props.onChange(e?e.value:"")}},{key:"handleLoadingError",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.props;if(t.onLoadingError===D)throw e;return t.onLoadingError({errors:[{value:e.message,type:"error"}]})}},{key:"render",value:function(){var e={id:this.props.id},t=(0,j.default)("anchorselectorfield",this.props.extraClass),n=this.getDropdownOptions(),r=this.props.value||"",a=f.default._t("CMS.ANCHOR_SELECT_OR_TYPE","Select or enter anchor");return h.default.createElement(R.Creatable,{searchable:!0,options:n,className:t,name:this.props.name,inputProps:e,onChange:this.handleChange,onBlurResetsInput:!0,value:r,placeholder:a,labelKey:"value"})}}]),t}(y.default);I.propTypes={extraClass:M.default.string,id:M.default.string,name:M.default.string.isRequired,onChange:M.default.func,value:M.default.string,attributes:M.default.oneOfType([M.default.object,M.default.array]),pageId:M.default.number,anchors:M.default.array,loadingState:M.default.oneOf(Object.keys(E.default).map(function(e){return E.default[e]})),onLoadingError:M.default.func,data:M.default.shape({endpoint:M.default.string,targetFieldName:M.default.string})},I.defaultProps={value:"",extraClass:"",onLoadingError:D,attributes:{}};var k=(0,v.connect)(l,d)(I);t.Component=I,t.ConnectedAnchorSelectorField=k,t.default=(0,F.default)(k)},"./client/src/legacy/CMSMain.AddForm.js":function(e,t,n){"use strict";var r=n(0);(function(e){return e&&e.__esModule?e:{default:e}})(r).default.entwine("ss",function(e){e(".TreeDropdownField").entwine({OldValue:null}),e("#Form_AddForm_ParentID_Holder .treedropdownfield").entwine({onmatch:function(){this._super(),e(".cms-add-form").updateTypeList()}}),e(".cms-add-form .parent-mode :input").entwine({onclick:function(e){var t=this.closest("form").find("#Form_AddForm_ParentID_Holder .TreeDropdownField");"top"==this.val()?(t.setOldValue(t.getValue()),t.setValue(0)):(t.setValue(t.getOldValue()||0),t.setOldValue(null)),t.refresh(),t.trigger("change")}}),e(".cms-add-form").entwine({ParentCache:{},onadd:function(){var t=this;this.find("#Form_AddForm_ParentID_Holder .TreeDropdownField").on("change",function(){t.updateTypeList()}),this.find(".SelectionGroup.parent-mode").on("change",function(){t.updateTypeList()}),"top"==e(".cms-add-form .parent-mode :input").val()&&this.updateTypeList()},loadCachedChildren:function(e){var t=this.getParentCache();return void 0!==t[e]?t[e]:null},saveCachedChildren:function(e,t){var n=this.getParentCache();n[e]=t,this.setParentCache(n)},updateTypeList:function(){var t=this.data("hints"),n=this.find("#Form_AddForm_ParentID"),r=this.find("input[name=ParentModeField]:checked").val(),a=n.data("metadata"),o="child"===r?n.getValue():null,i=a?a.ClassName:null,s=i&&"child"===r&&o?i:"Root",l=void 0!==t[s]?t[s]:null,d=this,c=l&&void 0!==l.defaultChild?l.defaultChild:null,u=[];if(o){if(this.hasClass("loading"))return;return this.addClass("loading"),null!==(u=this.loadCachedChildren(o))?(this.updateSelectionFilter(u,c),void this.removeClass("loading")):(e.ajax({url:d.data("childfilter"),data:{ParentID:o},success:function(e){d.saveCachedChildren(o,e),d.updateSelectionFilter(e,c)},complete:function(){d.removeClass("loading")}}),!1)}u=l&&void 0!==l.disallowedChildren?l.disallowedChildren:[],this.updateSelectionFilter(u,c)},updateSelectionFilter:function(t,n){var r=this.find("#Form_AddForm_PageType div.radio.selected")[0],a=!1,o=null;if(this.find("#Form_AddForm_PageType div.radio").each(function(n,i){var s=e(this).find("input").val(),l=-1===e.inArray(s,t);i===r&&l&&(a=!0),e(this).setEnabled(l),l||e(this).setSelected(!1),o=null===o?l:o&&l}),a)var i=e(r).parents("li:first");else if(n)var i=this.find("#Form_AddForm_PageType div.radio input[value="+n+"]").parents("li:first");else var i=this.find("#Form_AddForm_PageType div.radio:not(.disabled):first");i.setSelected(!0),i.siblings().setSelected(!1),this.find("#Form_AddForm_PageType div.radio:not(.disabled)").length?this.find("button[name=action_doAdd]").removeAttr("disabled"):this.find("button[name=action_doAdd]").attr("disabled","disabled"),this.find(".message-restricted")[o?"hide":"show"]()}}),e(".cms-add-form #Form_AddForm_PageType div.radio").entwine({onclick:function(e){this.setSelected(!0)},setSelected:function(e){var t=this.find("input");e&&!t.is(":disabled")?(this.siblings().setSelected(!1),this.toggleClass("selected",!0),t.prop("checked",!0)):(this.toggleClass("selected",!1),t.prop("checked",!1))},setEnabled:function(t){e(this).toggleClass("disabled",!t),t?e(this).find("input").removeAttr("disabled"):e(this).find("input").attr("disabled","disabled").removeAttr("checked")}}),e(".cms-content-addpage-button").entwine({onclick:function(t){var n,r=e(".cms-tree"),a=e(".cms-list"),o=0;if(r.is(":visible")){var i=r.jstree("get_selected");o=i?e(i[0]).data("id"):null}else{var s=a.find('input[name="Page[GridState]"]').val();s&&(o=parseInt(JSON.parse(s).ParentID,10))}var l,d={selector:this.data("targetPanel"),pjax:this.data("pjax")};o?(n=this.data("extraParams")?this.data("extraParams"):"",l=e.path.addSearchParams(i18n.sprintf(this.data("urlAddpage"),o),n)):l=this.attr("href"),e(".cms-container").loadPanel(l,null,d),t.preventDefault(),this.blur()}})})},"./client/src/legacy/CMSMain.EditForm.js":function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){return function(){var t=e.apply(this,arguments);return new Promise(function(e,n){function r(a,o){try{var i=t[a](o),s=i.value}catch(e){return void n(e)}if(!i.done)return Promise.resolve(s).then(function(e){r("next",e)},function(e){r("throw",e)});e(s)}return r("next")})}}var o=n(0),i=r(o),s=n(1),l=r(s),d=n("./node_modules/@silverstripe/reactstrap-confirm/dist/index.js"),c=r(d);i.default.entwine("ss",function(e){e(".cms-edit-form :input#Form_EditForm_ClassName").entwine({onchange:function(){alert(l.default._t("CMS.ALERTCLASSNAME"))}}),e(".cms-edit-form input[name=Title]").entwine({onmatch:function(){var t=this;t.data("OrigVal",t.val());var n=t.closest("form"),r=e("input:text[name=URLSegment]",n),a=e("input[name=LiveLink]",n);r.length>0&&(t._addActions(),this.on("change",function(n){var o=t.data("OrigVal"),i=t.val();t.data("OrigVal",i),0===r.val().indexOf(r.data("defaultUrl"))&&""==a.val()?t.updateURLSegment(i):e(".update",t.parent()).show().parent(".form__field-holder").addClass("input-group"),t.updateRelatedFields(i,o),t.updateBreadcrumbLabel(i)})),this._super()},onunmatch:function(){this._super()},updateRelatedFields:function(t,n){this.parents("form").find("input[name=MetaTitle], input[name=MenuTitle]").each(function(){var r=e(this);r.val()==n&&(r.val(t),r.updatedRelatedFields&&r.updatedRelatedFields())})},updateURLSegment:function(t){var n=e("input:text[name=URLSegment]",this.closest("form")),r=n.closest(".field.urlsegment"),a=e(".update",this.parent());r.update(t),a.is(":visible")&&a.hide().parent(".form__field-holder").removeClass("input-group")},updateBreadcrumbLabel:function(t){var n=(e(".cms-edit-form input[name=ID]").val(),e("span.cms-panel-link.crumb"));t&&""!=t&&n.text(t)},_addActions:function(){var t,n=this;t=e("<button />",{class:"update btn btn-outline-secondary form__field-update-url",text:l.default._t("CMS.UpdateURL"),type:"button",click:function(e){e.preventDefault(),n.updateURLSegment(n.val())}}),t.insertAfter(n),t.hide()}}),e(".cms-edit-form .parentTypeSelector").entwine({onmatch:function(){var e=this;this.find(":input[name=ParentType]").on("click",function(t){e._toggleSelection(t)}),this.find(".TreeDropdownField").on("change",function(t){e._changeParentId(t)}),this._changeParentId(),this._toggleSelection(),this._super()},onunmatch:function(){this._super()},_toggleSelection:function(t){var n=this.find(":input[name=ParentType]:checked").val(),r=this.find("#Form_EditForm_ParentID_Holder");"root"==n?this.find(":input[name=ParentID]").val(0):this.find(":input[name=ParentID]").val(this.find("#Form_EditForm_ParentType_subpage").data("parentIdValue")),"root"!=n?r.slideDown(400,function(){e(this).css("overflow","visible")}):r.slideUp()},_changeParentId:function(e){var t=this.find(":input[name=ParentID]").val();this.find("#Form_EditForm_ParentType_subpage").data("parentIdValue",t)}}),e(".cms-edit-form .btn-toolbar #Form_EditForm_action_doRollback, .cms-edit-form .btn-toolbar #Form_EditForm_action_rollback").entwine({onclick:function(e){if(this.is(":disabled"))return e.preventDefault(),!1;var t=this.parents("form:first").find(":input[name=Version]").val(),n=t?l.default.sprintf(l.default._t("CMS.RollbackToVersion","Do you really want to roll back to version #%s of this page?"),t):l.default._t("CMS.ConfirmRestoreFromLive","Are you sure you want to revert draft to when the page was last published?");return confirm(n)?(this.parents("form:first").addClass("loading"),this._super(e)):(e.preventDefault(),!1)}}),e(".cms-edit-form .btn-toolbar #Form_EditForm_action_archive:not(.homepage-warning)").entwine({onclick:function(e){var t=this.parents("form:first"),n="";return n=t.find("input[name=ArchiveWarningMessage]").val().replace(/\\n/g,"\n"),!!confirm(n)&&(this.parents("form:first").addClass("loading"),this._super(e))}}),e(".cms-edit-form .btn-toolbar #Form_EditForm_action_restore").entwine({onclick:function(e){var t=this.parents("form:first"),n=t.find(":input[name=Version]").val(),r="",a=this.data("toRoot");return r=l.default.sprintf(l.default._t(a?"CMS.RestoreToRoot":"CMS.Restore"),n),!!confirm(r)&&(this.parents("form:first").addClass("loading"),this._super(e))}}),e(".cms-edit-form .btn-toolbar #Form_EditForm_action_unpublish:not(.homepage-warning)").entwine({onclick:function(e){var t=this.parents("form:first"),n=t.find(":input[name=Version]").val(),r="";return r=l.default.sprintf(l.default._t("CMS.Unpublish"),n),!!confirm(r)&&(this.parents("form:first").addClass("loading"),this._super(e))}}),e(".cms-edit-form.changed").entwine({onmatch:function(t){this.find("button[data-text-alternate]").each(function(){var t=e(this),n=t.find(".btn__title"),r=t.data("textAlternate");r&&(t.data("textStandard",n.text()),n.text(r));var a=t.data("btnAlternate");a&&(t.data("btnStandard",t.attr("class")),t.attr("class",a),t.removeClass("btn-outline-secondary").addClass("btn-primary"));var o=t.data("btnAlternateAdd");o&&t.addClass(o);var i=t.data("btnAlternateRemove");i&&t.removeClass(i)}),this._super(t)},onunmatch:function(t){this.find("button[data-text-alternate]").each(function(){var t=e(this),n=t.find(".btn__title"),r=t.data("textStandard");r&&n.text(r);var a=t.data("btnStandard");a&&(t.attr("class",a),t.addClass("btn-outline-secondary").removeClass("btn-primary"));var o=t.data("btnAlternateAdd");o&&t.removeClass(o);var i=t.data("btnAlternateRemove");i&&t.addClass(i)}),this._super(t)}}),e(".cms-edit-form .btn-toolbar button[name=action_publish]").entwine({onbuttonafterrefreshalternate:function(){this.data("showingAlternate")?(this.addClass("btn-primary"),this.removeClass("btn-secondary")):(this.removeClass("btn-primary"),this.addClass("btn-secondary"))}}),e(".cms-edit-form .btn-toolbar button[name=action_save]").entwine({onbuttonafterrefreshalternate:function(){this.data("showingAlternate")?(this.addClass("btn-primary"),this.removeClass("btn-secondary")):(this.removeClass("btn-primary"),this.addClass("btn-secondary"))}}),e('.cms-edit-form.CMSPageSettingsController input[name="ParentType"]:checked').entwine({onmatch:function(){this.redraw(),this._super()},onunmatch:function(){this._super()},redraw:function(){var t=e(".cms-edit-form.CMSPageSettingsController #Form_EditForm_ParentID_Holder");"Form_EditForm_ParentType_root"==e(this).attr("id")?t.slideUp():t.slideDown()},onclick:function(){this.redraw()}}),"Form_EditForm_ParentType_root"==e('.cms-edit-form.CMSPageSettingsController input[name="ParentType"]:checked').attr("id")&&e(".cms-edit-form.CMSPageSettingsController #Form_EditForm_ParentID_Holder").hide();var t=!1;e(".cms-edit-form .btn-toolbar #Form_EditForm_action_unpublish.homepage-warning,.cms-edit-form .btn-toolbar #Form_EditForm_action_archive.homepage-warning,#Form_EditForm_URLSegment_Holder.homepage-warning .btn.update").entwine({onclick:function(){function e(e){return n.apply(this,arguments)}var n=a(regeneratorRuntime.mark(function e(n){var r;return regeneratorRuntime.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:if(!t){e.next=2;break}return e.abrupt("return",this._super(n));case 2:return n.stopPropagation(),r=l.default._t("CMS.RemoveHomePageWarningMessage","Warning: This page is the home page. By changing the URL segment visitors will not be able to view it."),e.next=6,(0,c.default)(r,{title:l.default._t("CMS.RemoveHomePageWarningTitle","Remove your home page?"),confirmLabel:l.default._t("CMS.RemoveHomePageWarningLabel","Remove"),confirmColor:"danger"});case 6:if(!e.sent){e.next=10;break}t=!0,this.trigger("click"),t=!1;case 10:return e.abrupt("return",!1);case 11:case"end":return e.stop()}},e,this)}));return e}()})})},"./client/src/legacy/CMSMain.Tree.js":function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){return function(){var t=e.apply(this,arguments);return new Promise(function(e,n){function r(a,o){try{var i=t[a](o),s=i.value}catch(e){return void n(e)}if(!i.done)return Promise.resolve(s).then(function(e){r("next",e)},function(e){r("throw",e)});e(s)}return r("next")})}}var o=n(0),i=r(o),s=n(1),l=r(s),d=n("./node_modules/@silverstripe/reactstrap-confirm/dist/index.js"),c=r(d);i.default.entwine("ss.tree",function(e){e(".cms-tree").entwine({fromDocument:{"oncontext_show.vakata":function(e){this.adjustContextClass()}},adjustContextClass:function(){var t=e("#vakata-contextmenu").find("ul ul");t.each(function(n){var r="1",a=e(t[n]).find("li").length;a>20?r="3":a>10&&(r="2"),e(t[n]).addClass("vakata-col-"+r).removeClass("right"),e(t[n]).find("li").on("mouseenter",function(t){e(this).parent("ul").removeClass("right")})})},showListViewFor:function(t){localStorage.setItem("ss.pages-view-type","listview");var n=this.closest(".cms-content-view"),r=n.data("url-listviewroot"),a=e.path.addSearchParams(r,{ParentID:t}),o=e("base").attr("href")||"";window.location.assign(o+a)},getTreeConfig:function(){var t=this,n=this._super();return this.getHints(),n.plugins.push("contextmenu"),n.contextmenu={items:function(n){var r={edit:{label:n.hasClass("edit-disabled")?l.default._t("CMS.EditPage","Edit page",100,"Used in the context menu when right-clicking on a page node in the CMS tree"):l.default._t("CMS.ViewPage","View page",100,"Used in the context menu when right-clicking on a page node in the CMS tree"),action:function(n){e(".cms-container").entwine(".ss").loadPanel(l.default.sprintf(t.data("urlEditpage"),n.data("id")))}}};n.hasClass("nochildren")||(r.showaslist={label:l.default._t("CMS.ShowAsList"),action:function(e){t.showListViewFor(e.data("id"))}});var a=(n.data("pagetype"),n.data("id")),o=n.find(">a .item").data("allowedchildren"),i={},s=!1;return e.each(o,function(n,r){s=!0,i["allowedchildren-"+r.ClassName]={label:'<span class="jstree-pageicon '+r.IconClass+'"></span>'+r.Title,_class:"class-"+r.ClassName.replace(/[^a-zA-Z0-9\-_:.]+/g,"_"),action:function(n){e(".cms-container").entwine(".ss").loadPanel(e.path.addSearchParams(l.default.sprintf(t.data("urlAddpage"),a,r.ClassName),t.data("extraParams")))}}}),s&&(r.addsubpage={label:l.default._t("CMS.AddSubPage","Add page under this page",100,"Used in the context menu when right-clicking on a page node in the CMS tree"),submenu:i}),n.hasClass("edit-disabled")||(r.duplicate={label:l.default._t("CMS.Duplicate"),submenu:[{label:l.default._t("CMS.ThisPageOnly"),action:function(n){e(".cms-container").entwine(".ss").loadPanel(e.path.addSearchParams(l.default.sprintf(t.data("urlDuplicate"),n.data("id")),t.data("extraParams")))}},{label:l.default._t("CMS.ThisPageAndSubpages"),action:function(n){e(".cms-container").entwine(".ss").loadPanel(e.path.addSearchParams(l.default.sprintf(t.data("urlDuplicatewithchildren"),n.data("id")),t.data("extraParams")))}}]}),r}},n},canMove:function(){function e(e){return t.apply(this,arguments)}var t=a(regeneratorRuntime.mark(function e(t){var n,r,a,o;return regeneratorRuntime.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:if(n=t.rslt.o.find(".homepage").first().length>0){e.next=3;break}return e.abrupt("return",!0);case 3:if(r=t.rslt.op.data("id"),a=t.rslt.np.data("id"),r!==a){e.next=7;break}return e.abrupt("return",!0);case 7:return o=l.default._t("CMS.RemoveHomePageWarningMessage","Warning: This page is the home page. By changing the URL segment visitors will not be able to view it."),e.next=10,(0,c.default)(o,{title:l.default._t("CMS.RemoveHomePageWarningTitle","Remove your home page?"),confirmLabel:l.default._t("CMS.RemoveHomePageWarningLabel","Remove"),confirmColor:"danger"});case 10:return e.abrupt("return",e.sent);case 11:case"end":return e.stop()}},e,this)}));return e}()}),e(".cms-tree a.jstree-clicked").entwine({onmatch:function(){var e=this,t=e.parents(".cms-tree-view-sidebar");if(e.offset().top<0||e.offset().top>t.height()-e.height()){var n=e.parent();n.prev().length&&(n=n.prev()),n.get(0).scrollIntoView()}}}),e(".cms-tree-filtered .clear-filter").entwine({onclick:function(){window.location=location.protocol+"//"+location.host+location.pathname}}),e(".cms-tree .subtree-list-link").entwine({onclick:function(e){e.preventDefault(),this.closest(".cms-tree").showListViewFor(this.data("id"))}})})},"./client/src/legacy/CMSMain.js":function(e,t,n){"use strict";var r=n(0);(function(e){return e&&e.__esModule?e:{default:e}})(r).default.entwine("ss",function(e){e(".cms-content-header-info").entwine({"from .cms-panel":{ontoggle:function(e){var t=this.closest(".cms-content").find(e.target);0!==t.length&&this.parent()[t.hasClass("collapsed")?"addClass":"removeClass"]("collapsed")}}}),e(".cms-panel-deferred.cms-content-view").entwine({onadd:function(){if(!this.data("no-ajax")){var e=localStorage.getItem("ss.pages-view-type")||"treeview";this.closest(".cms-content-tools").length>0&&(e="treeview");var t=this.data("url-"+e),n=localStorage.getItem("ss.pages-view-filtered");"string"==typeof n&&"false"===n.toLowerCase()&&(n=!1),localStorage.setItem("ss.pages-view-filtered",!1),this.data("deferredNoCache",n||"listview"===e),this.data("url",t+location.search),this._super()}}}),e(".js-injector-boot .search-holder--cms").entwine({search:function(e){localStorage.setItem("ss.pages-view-filtered",!0),this._super(e)}}),e(".cms .page-view-link").entwine({onclick:function(t){t.preventDefault();var n=e(this).data("view"),r=this.closest(".cms-content-view"),a=r.data("url-"+n),o=0!==r.closest(".cms-content-tools").length;if(localStorage.setItem("ss.pages-view-type",n),o&&"listview"===n){var i=e("base").attr("href")||"";return void window.location.assign(i+r.data("url-listviewroot"))}r.data("url",a+location.search),r.redraw()}}),e(".cms .cms-clear-filter").entwine({onclick:function(t){t.preventDefault(),window.location=e(this).prop("href")}}),e(".cms-content-toolbar").entwine({onmatch:function(){var t=this;this._super(),e.each(this.find(".cms-actions-buttons-row .tool-button"),function(){var n=e(this),r=n.data("toolid");n.hasClass("active"),void 0!==r&&(n.data("active",!1).removeClass("active"),e("#"+r).hide(),t.bindActionButtonEvents(n))})},onunmatch:function(){var t=this;this._super(),e.each(this.find(".cms-actions-buttons-row .tool-button"),function(){var n=e(this);t.unbindActionButtonEvents(n)})},bindActionButtonEvents:function(e){var t=this;e.on("click.cmsContentToolbar",function(n){t.showHideTool(e)})},unbindActionButtonEvents:function(e){e.off(".cmsContentToolbar")},showHideTool:function(t){var n=t.data("active"),r=t.data("toolid"),a=e("#"+r);e.each(this.find(".cms-actions-buttons-row .tool-button"),function(){var t=e(this),n=e("#"+t.data("toolid"));t.data("toolid")!==r&&(n.hide(),t.data("active",!1))}),t[n?"removeClass":"addClass"]("active"),a[n?"hide":"show"](),t.data("active",!n)}})})},"./client/src/legacy/CMSPageHistoryController.js":function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}var a=n(0),o=r(a),i=n(1),s=r(i);o.default.entwine("ss",function(e){e("#Form_VersionsForm").entwine({onmatch:function(){this._super()},onunmatch:function(){this._super()},onsubmit:function(t){t.preventDefault();var n=this.find(":input[name=ID]").val();if(!n)return!1;var r=null,a=null,o=null,i=this.find(":input[name=CompareMode]").is(":checked"),l=this.find("table input[type=checkbox]").filter(":checked");if(i){if(2!==l.length)return!1;a=l.eq(0).val(),o=l.eq(1).val(),r=s.default.sprintf(this.data("linkTmplCompare"),n,o,a)}else a=l.eq(0).val(),r=s.default.sprintf(this.data("linkTmplShow"),n,a);return e(".cms-container").loadPanel(r,"",{pjax:"CurrentForm"}),!0}}),e("#Form_VersionsForm input[name=ShowUnpublished]").entwine({onmatch:function(){this.toggle(),this._super()},onunmatch:function(){this._super()},onchange:function(){this.toggle()},toggle:function(){var t=e(this),n=t.parents("form").find("tr[data-published=false]");t.attr("checked")?n.removeClass("ui-helper-hidden").show():n.addClass("ui-helper-hidden").hide()._unselect()}}),e("#Form_VersionsForm tbody tr").entwine({onclick:function(){var e=this.parents("form").find(":input[name=CompareMode]").attr("checked"),t=this.siblings(".active");return e&&this.hasClass("active")?void this._unselect():e?t.length>1?void alert(s.default._t("CMS.ONLYSELECTTWO","You can only compare two versions at this time.")):(this._select(),void(1===t.length&&this.parents("form").submit())):(this._select(),t._unselect(),void this.parents("form").submit())},_unselect:function(){this.get(0).classList.remove("active"),this.find(":input[type=checkbox][checked]").attr("checked",!1)},_select:function(){this.addClass("active"),this.find(":input[type=checkbox]").attr("checked",!0)}})})},"./client/src/legacy/RedirectorPage.js":function(e,t,n){"use strict";var r=n(0);(function(e){return e&&e.__esModule?e:{default:e}})(r).default.entwine("ss",function(e){e("#Form_EditForm_RedirectionType input").entwine({onmatch:function(){e(this).attr("checked")&&this.toggle(),this._super()},onunmatch:function(){this._super()},onclick:function(){this.toggle()},toggle:function(){"Internal"==e(this).attr("value")?(e("#Form_EditForm_ExternalURL_Holder").hide(),e("#Form_EditForm_LinkToID_Holder").show(),e("#Form_EditForm_LinkToFile_Holder").hide()):"External"==e(this).attr("value")?(e("#Form_EditForm_ExternalURL_Holder").show(),e("#Form_EditForm_LinkToID_Holder").hide(),e("#Form_EditForm_LinkToFile_Holder").hide()):(e("#Form_EditForm_LinkToFile_Holder").show(),e("#Form_EditForm_ExternalURL_Holder").hide(),e("#Form_EditForm_LinkToID_Holder").hide())}})})},"./client/src/legacy/SiteTreeURLSegmentField.js":function(e,t,n){"use strict";var r=n(0);(function(e){return e&&e.__esModule?e:{default:e}})(r).default.entwine("ss",function(e){e(".field.urlsegment:not(.readonly)").entwine({MaxPreviewLength:55,Ellipsis:"...",onmatch:function(){this.find(":text").length&&this.toggleEdit(!1),this.redraw(),this._super()},redraw:function(){var e=this.find(":text"),t=decodeURI(e.data("prefix")+e.val()),n=t;t.length>this.getMaxPreviewLength()&&(n=this.getEllipsis()+t.substr(t.length-this.getMaxPreviewLength(),t.length)),this.find(".URL-link").attr("href",encodeURI(t+e.data("suffix"))).text(n)},toggleEdit:function(e){var t=this.find(":text");this.find(".preview-holder")[e?"hide":"show"](),this.find(".edit-holder")[e?"show":"hide"](),e&&(t.data("origval",t.val()),t.focus())},update:function(){var e=this,t=this.find(":text"),n=t.data("origval"),r=arguments[0],a=r&&""!==r?r:t.val();n!=a?(this.addClass("loading"),this.suggest(a,function(n){t.val(decodeURIComponent(n.value)),e.toggleEdit(!1),e.removeClass("loading"),e.redraw()})):(this.toggleEdit(!1),this.redraw())},cancel:function(){var e=this.find(":text");e.val(e.data("origval")),this.toggleEdit(!1)},suggest:function(t,n){var r=this,a=r.find(":text"),o=e.path.parseUrl(r.closest("form").attr("action")),i=o.hrefNoSearch+"/field/"+a.attr("name")+"/suggest/?value="+encodeURIComponent(t);o.search&&(i+="&"+o.search.replace(/^\?/,"")),e.ajax({url:i,success:function(e){n.apply(this,arguments)},error:function(e,t){e.statusText=e.responseText},complete:function(){r.removeClass("loading")}})}}),e(".field.urlsegment .text").entwine({onkeydown:function(e){13===e.keyCode&&(e.preventDefault(),this.closest(".field").update())}}),e(".field.urlsegment .edit").entwine({onclick:function(e){e.preventDefault(),this.closest(".field").toggleEdit(!0)}}),e(".field.urlsegment .update").entwine({onclick:function(e){e.preventDefault(),this.closest(".field").update()}}),e(".field.urlsegment .cancel").entwine({onclick:function(e){e.preventDefault(),this.closest(".field").cancel()}})})},"./client/src/state/anchorSelector/AnchorSelectorActionTypes.js":function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default={ANCHORSELECTOR_CURRENT_FIELD:"ANCHORSELECTOR_CURRENT_FIELD",ANCHORSELECTOR_UPDATED:"ANCHORSELECTOR_UPDATED",ANCHORSELECTOR_UPDATING:"ANCHORSELECTOR_UPDATING",ANCHORSELECTOR_UPDATE_FAILED:"ANCHORSELECTOR_UPDATE_FAILED"}},"./client/src/state/anchorSelector/AnchorSelectorActions.js":function(e,t,n){"use strict";function r(e){return{type:l.default.ANCHORSELECTOR_UPDATING,payload:{pageId:e}}}function a(e,t){var n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];return{type:l.default.ANCHORSELECTOR_UPDATED,payload:{pageId:e,anchors:t,cacheResult:n}}}function o(e,t,n){return{type:l.default.ANCHORSELECTOR_CURRENT_FIELD,payload:{pageId:e,anchors:t,fieldID:n}}}function i(e){return{type:l.default.ANCHORSELECTOR_UPDATE_FAILED,payload:{pageId:e}}}Object.defineProperty(t,"__esModule",{value:!0}),t.beginUpdating=r,t.updated=a,t.updatedCurrentField=o,t.updateFailed=i;var s=n("./client/src/state/anchorSelector/AnchorSelectorActionTypes.js"),l=function(e){return e&&e.__esModule?e:{default:e}}(s)},"./client/src/state/anchorSelector/AnchorSelectorReducer.js":function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t<e.length;t++)n[t]=e[t];return n}return Array.from(e)}function o(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:f,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,n=function(n,r){var o=t.payload.pageId;return(0,s.default)({pages:[].concat(a(e.pages.filter(function(e){return e.id!==o})),[{id:o,loadingState:n,anchors:r}]).sort(function(e,t){return e.id-t.id})})};switch(t.type){case d.default.ANCHORSELECTOR_UPDATING:return n(u.default.UPDATING,[]);case d.default.ANCHORSELECTOR_UPDATED:var r=t.payload,o=r.anchors,i=r.cacheResult,l=u.default.SUCCESS,c=u.default.DIRTY;return n(i?l:c,o);case d.default.ANCHORSELECTOR_CURRENT_FIELD:var p=t.payload.anchors;return n(u.default.FIELD_ONLY,p);case d.default.ANCHORSELECTOR_UPDATE_FAILED:return n(u.default.FAILED,[]);default:return e}}Object.defineProperty(t,"__esModule",{value:!0}),t.default=o;var i=n(13),s=r(i),l=n("./client/src/state/anchorSelector/AnchorSelectorActionTypes.js"),d=r(l),c=n("./client/src/state/anchorSelector/AnchorSelectorStates.js"),u=r(c),f=(0,s.default)({pages:[]})},"./client/src/state/anchorSelector/AnchorSelectorStates.js":function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default={SUCCESS:"SUCCESS",DIRTY:"DIRTY",FIELD_ONLY:"FIELD_ONLY",UPDATING:"UPDATING",FAILED:"FAILED"}},"./client/src/state/history/readOnePageQuery.js":function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.config=t.query=void 0;var r=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},a=function(e,t){return Object.freeze(Object.defineProperties(e,{raw:{value:Object.freeze(t)}}))}(["\nquery ReadHistoryViewerPage ($page_id: ID!, $limit: Int!, $offset: Int!) {\n readOnePage(\n versioning: {\n mode: ALL_VERSIONS\n },\n filter: {\n id: { eq: $page_id }\n }\n ) {\n id\n versions (limit: $limit, offset: $offset, sort: {\n version: DESC\n }) {\n pageInfo {\n totalCount\n }\n nodes {\n version\n absoluteLink\n author {\n firstName\n surname\n }\n publisher {\n firstName\n surname\n }\n deleted\n draft\n published\n liveVersion\n latestDraftVersion\n lastEdited\n }\n }\n }\n}\n"],["\nquery ReadHistoryViewerPage ($page_id: ID!, $limit: Int!, $offset: Int!) {\n readOnePage(\n versioning: {\n mode: ALL_VERSIONS\n },\n filter: {\n id: { eq: $page_id }\n }\n ) {\n id\n versions (limit: $limit, offset: $offset, sort: {\n version: DESC\n }) {\n pageInfo {\n totalCount\n }\n nodes {\n version\n absoluteLink\n author {\n firstName\n surname\n }\n publisher {\n firstName\n surname\n }\n deleted\n draft\n published\n liveVersion\n latestDraftVersion\n lastEdited\n }\n }\n }\n}\n"]),o=n(4),i=n(10),s=function(e){return e&&e.__esModule?e:{default:e}}(i),l=(0,s.default)(a),d={options:function(e){var t=e.recordId,n=e.limit;return{variables:{limit:n,offset:((e.page||1)-1)*n,page_id:t},fetchPolicy:"network-only"}},props:function(e){var t=e.data,n=t.error,a=t.refetch,o=t.readOnePage,i=t.loading,s=e.ownProps,l=s.actions,d=void 0===l?{versions:{}}:l,c=s.limit,u=s.recordId,f=o||null,p=n&&n.graphQLErrors&&n.graphQLErrors.map(function(e){return e.message});return{loading:i||!f,versions:f,graphQLErrors:p,actions:r({},d,{versions:r({},f,{goToPage:function(e){a({offset:((e||1)-1)*c,limit:c,page_id:u})}})})}}};t.query=l,t.config=d,t.default=(0,o.graphql)(l,d)},"./client/src/state/history/rollbackPageMutation.js":function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.config=t.mutation=void 0;var r=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},a=function(e,t){return Object.freeze(Object.defineProperties(e,{raw:{value:Object.freeze(t)}}))}(["\nmutation rollbackPage($id:ID!, $toVersion:Int!) {\n rollbackPage(\n id: $id\n toVersion: $toVersion\n ) {\n id\n }\n}\n"],["\nmutation rollbackPage($id:ID!, $toVersion:Int!) {\n rollbackPage(\n id: $id\n toVersion: $toVersion\n ) {\n id\n }\n}\n"]),o=n(4),i=n(10),s=function(e){return e&&e.__esModule?e:{default:e}}(i),l=(0,s.default)(a),d={props:function(e){var t=e.mutate,n=e.ownProps.actions,a=function(e,n){return t({variables:{id:e,toVersion:n}})};return{actions:r({},n,{rollbackPage:a,revertToVersion:a})}},options:{refetchQueries:["ReadHistoryViewerPage"]}};t.mutation=l,t.config=d,t.default=(0,o.graphql)(l,d)},"./node_modules/@babel/runtime/helpers/extends.js":function(e,t){function n(){return e.exports=n=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},e.exports.default=e.exports,e.exports.__esModule=!0,n.apply(this,arguments)}e.exports=n,e.exports.default=e.exports,e.exports.__esModule=!0},"./node_modules/@babel/runtime/helpers/inheritsLoose.js":function(e,t,n){function r(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,a(e,t)}var a=n("./node_modules/@babel/runtime/helpers/setPrototypeOf.js");e.exports=r,e.exports.default=e.exports,e.exports.__esModule=!0},"./node_modules/@babel/runtime/helpers/interopRequireDefault.js":function(e,t){function n(e){return e&&e.__esModule?e:{default:e}}e.exports=n,e.exports.default=e.exports,e.exports.__esModule=!0},"./node_modules/@babel/runtime/helpers/interopRequireWildcard.js":function(e,t,n){function r(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(r=function(e){return e?n:t})(e)}function a(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!==o(e)&&"function"!=typeof e)return{default:e};var n=r(t);if(n&&n.has(e))return n.get(e);var a={},i=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var s in e)if("default"!==s&&Object.prototype.hasOwnProperty.call(e,s)){var l=i?Object.getOwnPropertyDescriptor(e,s):null;l&&(l.get||l.set)?Object.defineProperty(a,s,l):a[s]=e[s]}return a.default=e,n&&n.set(e,a),a}var o=n("./node_modules/@babel/runtime/helpers/typeof.js").default;e.exports=a,e.exports.default=e.exports,e.exports.__esModule=!0},"./node_modules/@babel/runtime/helpers/setPrototypeOf.js":function(e,t){function n(t,r){return e.exports=n=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},e.exports.default=e.exports,e.exports.__esModule=!0,n(t,r)}e.exports=n,e.exports.default=e.exports,e.exports.__esModule=!0},"./node_modules/@babel/runtime/helpers/typeof.js":function(e,t){function n(t){"@babel/helpers - typeof";return"function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?(e.exports=n=function(e){return typeof e},e.exports.default=e.exports,e.exports.__esModule=!0):(e.exports=n=function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e.exports.default=e.exports,e.exports.__esModule=!0),n(t)}e.exports=n,e.exports.default=e.exports,e.exports.__esModule=!0},"./node_modules/@silverstripe/reactstrap-confirm/dist/Confirmation.js":function(e,t,n){"use strict";var r=n("./node_modules/@babel/runtime/helpers/interopRequireWildcard.js"),a=n("./node_modules/@babel/runtime/helpers/interopRequireDefault.js");t.__esModule=!0,t.default=void 0;var o=a(n("./node_modules/@babel/runtime/helpers/inheritsLoose.js")),i=r(n(2)),s=a(n(11)),l=n(17),d=function(e){function t(t){var n;return n=e.call(this,t)||this,n.state={isOpen:!0},n}return(0,o.default)(t,e),t.prototype.render=function(){var e=this,t=this.props,n=t.onConfirm,r=t.onCancel,a=t.title,o=t.body,s=t.confirmLabel,d=t.confirmColor,c=t.dismissLabel,u=t.showDismissButton,f=this.state.isOpen,p=function(){"function"==typeof r&&r(),e.setState({isOpen:!1})},h=function(){n(),e.setState({isOpen:!1})};return i.default.createElement(l.Modal,{isOpen:f,toggle:p},a&&i.default.createElement(l.ModalHeader,{toggle:p},a),i.default.createElement(l.ModalBody,null,o),i.default.createElement(l.ModalFooter,null,i.default.createElement(l.Button,{color:d,onClick:h},s),(u||!a)&&i.default.createElement(l.Button,{onClick:p},c||"Cancel")))},t}(i.Component);d.propTypes={onConfirm:s.default.func.isRequired,body:s.default.string.isRequired,onCancel:s.default.func,title:s.default.string,confirmLabel:s.default.string,confirmColor:s.default.string,dismissLabel:s.default.string},d.defaultProps={confirmLabel:"Confirm",confirmColor:"primary"};var c=d;t.default=c},"./node_modules/@silverstripe/reactstrap-confirm/dist/confirm.js":function(e,t,n){"use strict";var r=n("./node_modules/@babel/runtime/helpers/interopRequireDefault.js");t.__esModule=!0,t.default=void 0;var a=r(n("./node_modules/@babel/runtime/helpers/extends.js")),o=r(n(2)),i=r(n(5)),s=r(n("./node_modules/@silverstripe/reactstrap-confirm/dist/Confirmation.js")),l=function(e,t,n,r,l){void 0===t&&(t={}),void 0===n&&(n=document.body),void 0===r&&(r=350);var d=l||s.default,c=n.appendChild(document.createElement("div"));return new Promise(function(s){var l=function(e){return function(){s(e),setTimeout(function(){i.default.unmountComponentAtNode(c),setTimeout(function(){return n.removeChild(c)})},r)}};i.default.render(o.default.createElement(d,(0,a.default)({},t,{onConfirm:l(!0),onCancel:l(!1),body:e})),c)})},d=l;t.default=d},"./node_modules/@silverstripe/reactstrap-confirm/dist/index.js":function(e,t,n){"use strict";var r=n("./node_modules/@babel/runtime/helpers/interopRequireDefault.js");t.__esModule=!0,t.default=void 0;var a=r(n("./node_modules/@silverstripe/reactstrap-confirm/dist/confirm.js")),o=r(n("./node_modules/@silverstripe/reactstrap-confirm/dist/Confirmation.js"));t.Confirmation=o.default;var i=a.default;t.default=i},0:function(e,t){e.exports=jQuery},1:function(e,t){e.exports=i18n},10:function(e,t){e.exports=GraphQLTag},11:function(e,t){e.exports=PropTypes},12:function(e,t){e.exports=Redux},13:function(e,t){e.exports=DeepFreezeStrict},14:function(e,t){e.exports=FieldHolder},15:function(e,t){e.exports=IsomorphicFetch},16:function(e,t){e.exports=ReactSelect},17:function(e,t){e.exports=Reactstrap},18:function(e,t){e.exports=ReduxForm},19:function(e,t){e.exports=SilverStripeComponent},2:function(e,t){e.exports=React},20:function(e,t){e.exports=classnames},21:function(e,t){e.exports=getFormState},3:function(e,t){e.exports=Injector},4:function(e,t){e.exports=ReactApollo},5:function(e,t){e.exports=ReactDom},6:function(e,t){e.exports=ReactRedux}});

View File

@ -1 +1 @@
#SilverStripeNavigator{position:fixed;bottom:0;left:0;width:100%;border-top:2px solid #d4d0c8;background-color:#81858d;height:22px}#SilverStripeNavigator *{font-family:Arial,Helvetica,sans-serif;font-size:10px !important}#SilverStripeNavigator .holder{text-align:center;padding-top:4px;padding-left:3px;padding-right:6px;color:#fff;border-top:1px solid #555}#SilverStripeNavigator #logInStatus{float:right}#SilverStripeNavigator #switchView{float:left}#SilverStripeNavigator a{color:#fff;background-color:rgba(0,0,0,0);text-decoration:underline}#SilverStripeNavigator a:hover{background-color:rgba(0,0,0,0)}#SilverStripeNavigator .bottomTabs a{margin-right:8px;text-decoration:underline}#SilverStripeNavigator .bottomTabs a.current{font-weight:bold;text-decoration:none}#SilverStripeNavigatorMessage{font-family:"Lucida Grande",Verdana,Arial,"sans-serif";position:fixed;z-index:1000;right:20px;top:40px;padding:10px;border-color:#c99;color:#fff;background-color:#c00;border:1px solid #000}#SilverStripeNavigatorLinkPopup{display:none;position:absolute;top:-60px;height:50px;width:350px;left:200px;background-color:#fff;border:1px solid #000;z-index:100;color:#000;padding:5px}#SilverStripeNavigatorLinkPopup input{width:250px}#SilverStripeNavigatorLinkPopup a.close{color:blue;text-align:right;width:80%;border:none !important;cursor:pointer}
#SilverStripeNavigator{position:fixed;bottom:0;left:0;width:100%;border-top:2px solid #d4d0c8;background-color:#81858d;height:22px}#SilverStripeNavigator *{font-family:Arial,Helvetica,sans-serif;font-size:10px!important}#SilverStripeNavigator .holder{text-align:center;padding-top:4px;padding-left:3px;padding-right:6px;color:#fff;border-top:1px solid #555}#SilverStripeNavigator #logInStatus{float:right}#SilverStripeNavigator #switchView{float:left}#SilverStripeNavigator a{color:#fff;text-decoration:underline}#SilverStripeNavigator a,#SilverStripeNavigator a:hover{background-color:transparent}#SilverStripeNavigator .bottomTabs a{margin-right:8px;text-decoration:underline}#SilverStripeNavigator .bottomTabs a.current{font-weight:700;text-decoration:none}#SilverStripeNavigatorMessage{font-family:Lucida Grande,Verdana,Arial,"sans-serif";position:fixed;z-index:1000;right:20px;top:40px;padding:10px;border-color:#c99;color:#fff;background-color:#c00;border:1px solid #000}#SilverStripeNavigatorLinkPopup{display:none;position:absolute;top:-60px;height:50px;width:350px;left:200px;background-color:#fff;border:1px solid #000;z-index:100;color:#000;padding:5px}#SilverStripeNavigatorLinkPopup input{width:250px}#SilverStripeNavigatorLinkPopup a.close{color:blue;text-align:right;width:80%;border:none!important;cursor:pointer}

View File

@ -1 +1 @@
#cms-page-history-versions tr.loading{color:#999}#cms-page-history-versions tr.loading td:hover{cursor:none}#cms-page-history-versions td:hover{cursor:pointer}#cms-content-treeview .cms-tree-expand-trigger,#cms-content-listview .cms-tree-expand-trigger{display:none}.cms-content-tools #cms-content-treeview .cms-content-toolbar{border-bottom:none;box-shadow:none;margin-bottom:0}.cms-content-tools #cms-content-treeview .cms-tree-expand-trigger{display:block;float:left;margin:0 0 2px 0}.cms-content-tools #cms-content-treeview .cms-tree-expand-trigger span.ui-button-text{padding-right:8px}.cms-content-tools #cms-content-treeview .cms-tree .badge{display:none}.cms-content-tools #cms-content-treeview .cms-tree a:hover>.text>.badge,.cms-content-tools #cms-content-treeview .cms-tree .jstree-clicked>.text>.badge{display:inline-block}#cms-content-tools-CMSMain .search-form{right:0}.cms-list .cms-list__item-breadcrumbs{margin-left:21px;margin-bottom:0;font-size:.9em;word-break:break-word}.cms-content-toolbar .view-controls{margin-top:0}.cms-content-toolbar .view-controls .page-view-link{display:inline-block;margin-right:-5px}.cms-content-toolbar .view-controls.view-controls--treeview .font-icon-tree,.cms-content-toolbar .view-controls.view-controls--listview .font-icon-list{display:none}#pages-controller-cms-content,#pages-controller-cms-content+.cms-preview{width:50%}.field.urlsegment .input-group{width:auto;flex-wrap:nowrap}.field.urlsegment.loading .form__field-label{background-image:url(data:image/gif;base64,R0lGODlhEAAQAPQAAP///wpakvj6+z9+qYivyg9dlC5yotfk7KvG2R9om3umxGubveXt8py80sjZ5k+IsFyRtgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAAFUCAgjmRpnqUwFGwhKoRgqq2YFMaRGjWA8AbZiIBbjQQ8AmmFUJEQhQGJhaKOrCksgEla+KIkYvC6SJKQOISoNSYdeIk1ayA8ExTyeR3F749CACH5BAkKAAAALAAAAAAQABAAAAVoICCKR9KMaCoaxeCoqEAkRX3AwMHWxQIIjJSAZWgUEgzBwCBAEQpMwIDwY1FHgwJCtOW2UDWYIDyqNVVkUbYr6CK+o2eUMKgWrqKhj0FrEM8jQQALPFA3MAc8CQSAMA5ZBjgqDQmHIyEAIfkECQoAAAAsAAAAABAAEAAABWAgII4j85Ao2hRIKgrEUBQJLaSHMe8zgQo6Q8sxS7RIhILhBkgumCTZsXkACBC+0cwF2GoLLoFXREDcDlkAojBICRaFLDCOQtQKjmsQSubtDFU/NXcDBHwkaw1cKQ8MiyEAIfkECQoAAAAsAAAAABAAEAAABVIgII5kaZ6AIJQCMRTFQKiDQx4GrBfGa4uCnAEhQuRgPwCBtwK+kCNFgjh6QlFYgGO7baJ2CxIioSDpwqNggWCGDVVGphly3BkOpXDrKfNm/4AhACH5BAkKAAAALAAAAAAQABAAAAVgICCOZGmeqEAMRTEQwskYbV0Yx7kYSIzQhtgoBxCKBDQCIOcoLBimRiFhSABYU5gIgW01pLUBYkRItAYAqrlhYiwKjiWAcDMWY8QjsCf4DewiBzQ2N1AmKlgvgCiMjSQhACH5BAkKAAAALAAAAAAQABAAAAVfICCOZGmeqEgUxUAIpkA0AMKyxkEiSZEIsJqhYAg+boUFSTAkiBiNHks3sg1ILAfBiS10gyqCg0UaFBCkwy3RYKiIYMAC+RAxiQgYsJdAjw5DN2gILzEEZgVcKYuMJiEAOwAAAAAAAAAAAA==);background-repeat:no-repeat;background-position:right center;padding-right:20px}.field.urlsegment .URL-link{padding-top:8px;display:inline-block}.field.urlsegment input.text{flex:0 1 250px}.field.urlsegment .input-group-append{z-index:2}.field.urlsegment .input-group-append:last-of-type{z-index:1}.field.urlsegment .input-group-append .btn{margin-top:0;padding-top:.5385rem;padding-bottom:.5385rem}.field.urlsegment .help{margin-left:0}.field.urlsegment .edit-holder{display:none}.field.urlsegment .edit-holder .form__field-description{clear:both}.form__field-update-url{border-radius:0 3px 3px 0;margin-right:0px;margin-left:-1px}.anchorselectorfield__menu{z-index:20000}
#cms-page-history-versions tr.loading{color:#999}#cms-page-history-versions tr.loading td:hover{cursor:none}#cms-page-history-versions td:hover{cursor:pointer}.CMSPageHistoryController{overflow:hidden}.CMSPageHistoryController ins{background-color:#dfd;padding:2px;text-decoration:none}.CMSPageHistoryController del{background-color:#fdd;padding:2px;color:#f44}.CMSPageHistoryController .htmleditorfield.readonly img{max-width:100%;height:auto}.CMSPageHistoryController .cms-content-tools.collapsed{overflow:hidden}#cms-content-listview .cms-tree-expand-trigger,#cms-content-treeview .cms-tree-expand-trigger{display:none}.cms-content-tools #cms-content-treeview .cms-content-toolbar{border-bottom:none;-webkit-box-shadow:none;box-shadow:none;margin-bottom:0}.cms-content-tools #cms-content-treeview .cms-tree-expand-trigger{display:block;float:left;margin:0 0 2px}.cms-content-tools #cms-content-treeview .cms-tree-expand-trigger span.ui-button-text{padding-right:8px}.cms-content-tools #cms-content-treeview .cms-tree .badge{display:none}.cms-content-tools #cms-content-treeview .cms-tree .jstree-clicked>.text>.badge,.cms-content-tools #cms-content-treeview .cms-tree a:hover>.text>.badge{display:inline-block}#cms-content-tools-CMSMain .search-form{right:0}.cms-list .cms-list__item-breadcrumbs{margin-left:21px;margin-bottom:0;font-size:.9em;word-break:break-word}.cms-content-toolbar .view-controls{margin-top:0}.cms-content-toolbar .view-controls .page-view-link{display:inline-block;margin-right:-5px}.cms-content-toolbar .view-controls.view-controls--listview .font-icon-list,.cms-content-toolbar .view-controls.view-controls--treeview .font-icon-tree{display:none}#pages-controller-cms-content,#pages-controller-cms-content+.cms-preview{width:50%}.field.urlsegment .input-group{width:auto;-webkit-flex-wrap:nowrap;flex-wrap:nowrap}.field.urlsegment.loading .form__field-label{background-image:url(data:image/gif;base64,R0lGODlhEAAQAPQAAP///wpakvj6+z9+qYivyg9dlC5yotfk7KvG2R9om3umxGubveXt8py80sjZ5k+IsFyRtgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAAFUCAgjmRpnqUwFGwhKoRgqq2YFMaRGjWA8AbZiIBbjQQ8AmmFUJEQhQGJhaKOrCksgEla+KIkYvC6SJKQOISoNSYdeIk1ayA8ExTyeR3F749CACH5BAkKAAAALAAAAAAQABAAAAVoICCKR9KMaCoaxeCoqEAkRX3AwMHWxQIIjJSAZWgUEgzBwCBAEQpMwIDwY1FHgwJCtOW2UDWYIDyqNVVkUbYr6CK+o2eUMKgWrqKhj0FrEM8jQQALPFA3MAc8CQSAMA5ZBjgqDQmHIyEAIfkECQoAAAAsAAAAABAAEAAABWAgII4j85Ao2hRIKgrEUBQJLaSHMe8zgQo6Q8sxS7RIhILhBkgumCTZsXkACBC+0cwF2GoLLoFXREDcDlkAojBICRaFLDCOQtQKjmsQSubtDFU/NXcDBHwkaw1cKQ8MiyEAIfkECQoAAAAsAAAAABAAEAAABVIgII5kaZ6AIJQCMRTFQKiDQx4GrBfGa4uCnAEhQuRgPwCBtwK+kCNFgjh6QlFYgGO7baJ2CxIioSDpwqNggWCGDVVGphly3BkOpXDrKfNm/4AhACH5BAkKAAAALAAAAAAQABAAAAVgICCOZGmeqEAMRTEQwskYbV0Yx7kYSIzQhtgoBxCKBDQCIOcoLBimRiFhSABYU5gIgW01pLUBYkRItAYAqrlhYiwKjiWAcDMWY8QjsCf4DewiBzQ2N1AmKlgvgCiMjSQhACH5BAkKAAAALAAAAAAQABAAAAVfICCOZGmeqEgUxUAIpkA0AMKyxkEiSZEIsJqhYAg+boUFSTAkiBiNHks3sg1ILAfBiS10gyqCg0UaFBCkwy3RYKiIYMAC+RAxiQgYsJdAjw5DN2gILzEEZgVcKYuMJiEAOwAAAAAAAAAAAA==);background-repeat:no-repeat;background-position:100%;padding-right:20px}.field.urlsegment .URL-link{padding-top:8px;display:inline-block}.field.urlsegment input.text{-webkit-box-flex:0;-webkit-flex:0 1 250px;flex:0 1 250px}.field.urlsegment .input-group-append{z-index:2}.field.urlsegment .input-group-append:last-of-type{z-index:1}.field.urlsegment .input-group-append .btn{margin-top:0;padding-top:.5385rem;padding-bottom:.5385rem}.field.urlsegment .help{margin-left:0}.field.urlsegment .edit-holder{display:none}.field.urlsegment .edit-holder .form__field-description{clear:both}.form__field-update-url{border-radius:0 3px 3px 0;margin-right:0;margin-left:-1px}

View File

@ -2,6 +2,7 @@ require('../legacy/CMSMain.AddForm');
require('../legacy/CMSMain.EditForm');
require('../legacy/CMSMain');
require('../legacy/CMSMain.Tree');
require('../legacy/CMSPageHistoryController');
require('../legacy/RedirectorPage');
require('../legacy/SiteTreeURLSegmentField');

View File

@ -8,8 +8,7 @@ import SilverStripeComponent from 'lib/SilverStripeComponent';
import * as anchorSelectorActions from 'state/anchorSelector/AnchorSelectorActions';
import anchorSelectorStates from 'state/anchorSelector/AnchorSelectorStates';
import fieldHolder from 'components/FieldHolder/FieldHolder';
import CreatableSelect from 'react-select/creatable';
import EmotionCssCacheProvider from 'containers/EmotionCssCacheProvider/EmotionCssCacheProvider';
import { Creatable } from 'react-select';
import getFormState from 'lib/getFormState';
import classnames from 'classnames';
import PropTypes from 'prop-types';
@ -118,27 +117,26 @@ class AnchorSelectorField extends SilverStripeComponent {
}
render() {
const { extraClass, CreatableSelectComponent } = this.props;
const className = classnames('anchorselectorfield', extraClass);
const inputProps = {
id: this.props.id,
};
const className = classnames('anchorselectorfield', this.props.extraClass);
const options = this.getDropdownOptions();
const rawValue = this.props.value || '';
const value = this.props.value || '';
const placeholder = i18n._t('CMS.ANCHOR_SELECT_OR_TYPE', 'Select or enter anchor');
return (
<EmotionCssCacheProvider>
<CreatableSelectComponent
isSearchable
isClearable
options={options}
className={className}
name={this.props.name}
onChange={this.handleChange}
value={{ value: rawValue }}
noOptionsMessage={() => i18n._t('CMS.ANCHOR_NO_OPTIONS', 'No options')}
placeholder={placeholder}
getOptionLabel={({ value }) => value}
classNamePrefix="anchorselectorfield"
/>
</EmotionCssCacheProvider>
<Creatable
searchable
options={options}
className={className}
name={this.props.name}
inputProps={inputProps}
onChange={this.handleChange}
onBlurResetsInput
value={value}
placeholder={placeholder}
labelKey="value"
/>
);
}
}
@ -153,8 +151,8 @@ AnchorSelectorField.propTypes = {
pageId: PropTypes.number,
anchors: PropTypes.array,
loadingState: PropTypes.oneOf(Object
.keys(anchorSelectorStates)
.map((key) => anchorSelectorStates[key])),
.keys(anchorSelectorStates)
.map((key) => anchorSelectorStates[key])),
onLoadingError: PropTypes.func,
data: PropTypes.shape({
endpoint: PropTypes.string,
@ -167,7 +165,6 @@ AnchorSelectorField.defaultProps = {
extraClass: '',
onLoadingError: noop,
attributes: {},
CreatableSelectComponent: CreatableSelect
};
function mapStateToProps(state, ownProps) {

View File

@ -1,3 +0,0 @@
.anchorselectorfield__menu {
z-index: 20000;
}

View File

@ -1,129 +1,103 @@
/* global jest, test, describe, beforeEach, it, expect, setTimeout */
import React from 'react';
import anchorSelectorStates from 'state/anchorSelector/AnchorSelectorStates';
import { render, screen } from '@testing-library/react';
import { Component as AnchorSelectorField } from '../AnchorSelectorField';
/* global jest, describe, beforeEach, it, expect, setTimeout */
jest.mock('isomorphic-fetch', () =>
() => Promise.resolve({
json: () => ['anchor3', 'anchor4'],
}));
}));
jest.mock('i18n');
function makeProps(obj = {}) {
return {
id: 'Form_Test',
name: 'Test',
data: {
endpoint: 'url-callback',
},
pageId: 4,
anchors: ['anchor1', 'anchor2'],
value: 'selectedanchor',
loadingState: anchorSelectorStates.SUCCESS,
CreatableSelectComponent: ({ options }) => (
<div data-testid="test-creatable-select">
{options.map(option => <div key={option.value} data-option={option.value}/>)}
</div>
),
...obj,
};
}
import React from 'react';
import ReactTestUtils from 'react-dom/test-utils';
import { Component as AnchorSelectorField } from '../AnchorSelectorField';
import anchorSelectorStates from 'state/anchorSelector/AnchorSelectorStates';
test('AnchorSelectorField componentDidMount() Loads dirty selectors', async () => {
const beginUpdating = jest.fn();
render(<AnchorSelectorField {...makeProps({
loadingState: anchorSelectorStates.DIRTY,
actions: {
anchorSelector: {
beginUpdating,
updated: () => {},
updateFailed: () => {},
},
},
})}
/>);
await screen.findByTestId('test-creatable-select');
expect(beginUpdating).toBeCalledWith(4);
});
describe('AnchorSelectorField', () => {
let props = null;
let field = null;
test('AnchorSelectorField Merges value with page anchors', async () => {
const beginUpdating = jest.fn();
render(<AnchorSelectorField {...makeProps({
loadingState: anchorSelectorStates.DIRTY,
actions: {
anchorSelector: {
beginUpdating,
updated: () => {},
updateFailed: () => {},
beforeEach(() => {
props = {
id: 'Form_Test',
name: 'Test',
data: {
endpoint: 'url-callback',
},
},
})}
/>);
const select = await screen.findByTestId('test-creatable-select');
const options = select.querySelectorAll('[data-option]');
expect(options).toHaveLength(3);
expect(options[0].getAttribute('data-option')).toBe('selectedanchor');
expect(options[1].getAttribute('data-option')).toBe('anchor1');
expect(options[2].getAttribute('data-option')).toBe('anchor2');
});
pageId: 4,
anchors: ['anchor1', 'anchor2'],
value: 'selectedanchor',
loadingState: anchorSelectorStates.SUCCESS,
actions: {
anchorSelector: {
beginUpdating: jest.fn(),
updated: jest.fn(),
updateFailed: jest.fn(),
},
},
};
});
test('AnchorSelectorField componentDidMount() Does not load success selectors', async () => {
const beginUpdating = jest.fn();
render(<AnchorSelectorField {...makeProps({
loadingState: anchorSelectorStates.SUCCESS,
actions: {
anchorSelector: {
beginUpdating,
updated: () => {},
updateFailed: () => {},
},
},
})}
/>);
await screen.findByTestId('test-creatable-select');
expect(beginUpdating).not.toBeCalled();
});
describe('componentDidMount()', () => {
it('Loads dirty selectors', () => {
props.loadingState = anchorSelectorStates.DIRTY;
field = ReactTestUtils.renderIntoDocument(<AnchorSelectorField {...props} />);
expect(props.actions.anchorSelector.beginUpdating)
.toHaveBeenCalledWith(4);
});
it('Does not load success selectors', () => {
props.loadingState = anchorSelectorStates.SUCCESS;
field = ReactTestUtils.renderIntoDocument(<AnchorSelectorField {...props} />);
expect(props.actions.anchorSelector.beginUpdating)
.not
.toHaveBeenCalled();
});
});
test('AnchorSelectorField ensurePagesLoaded Triggers loading on dirty', async () => {
const beginUpdating = jest.fn();
const updated = jest.fn();
const updateFailed = jest.fn();
render(<AnchorSelectorField {...makeProps({
loadingState: anchorSelectorStates.DIRTY,
actions: {
anchorSelector: {
beginUpdating,
updated,
updateFailed,
},
},
})}
/>);
await screen.findByTestId('test-creatable-select');
expect(beginUpdating).toBeCalledWith(4);
expect(updated).toBeCalledWith(4, ['anchor3', 'anchor4']);
expect(updateFailed).not.toBeCalled();
});
describe('getDropdownOptions()', () => {
it('Merges value with page anchors', () => {
field = ReactTestUtils.renderIntoDocument(<AnchorSelectorField {...props} />);
expect(field.getDropdownOptions()).toEqual([
{ value: 'selectedanchor' },
{ value: 'anchor1' },
{ value: 'anchor2' },
]);
});
});
test('AnchorSelectorField ensurePagesLoaded Does not trigger updating', async () => {
const beginUpdating = jest.fn();
const updated = jest.fn();
const updateFailed = jest.fn();
render(<AnchorSelectorField {...makeProps({
loadingState: anchorSelectorStates.UPDATING,
actions: {
anchorSelector: {
beginUpdating,
updated,
updateFailed,
},
},
})}
/>);
await screen.findByTestId('test-creatable-select');
expect(beginUpdating).not.toBeCalled();
expect(updated).not.toBeCalled();
expect(updateFailed).not.toBeCalled();
describe('ensurePagesLoaded', () => {
it('Triggers loading on dirty', () => {
props.loadingState = anchorSelectorStates.DIRTY;
field = ReactTestUtils.renderIntoDocument(<AnchorSelectorField {...props} />);
return field
.ensurePagesLoaded()
.then((result) => {
expect(props.actions.anchorSelector.beginUpdating)
.toHaveBeenCalledWith(4);
expect(props.actions.anchorSelector.updated)
.toHaveBeenCalledWith(4, ['anchor3', 'anchor4']);
expect(props.actions.anchorSelector.updateFailed)
.not
.toHaveBeenCalled();
expect(result).toEqual(['anchor3', 'anchor4']);
});
});
it('Does not trigger updating', () => {
props.loadingState = anchorSelectorStates.UPDATING;
field = ReactTestUtils.renderIntoDocument(<AnchorSelectorField {...props} />);
return field
.ensurePagesLoaded()
.then((result) => {
expect(props.actions.anchorSelector.beginUpdating)
.not
.toHaveBeenCalled();
expect(props.actions.anchorSelector.updated)
.not
.toHaveBeenCalled();
expect(props.actions.anchorSelector.updateFailed)
.not
.toHaveBeenCalled();
expect(result).toBe(undefined);
});
});
});
});

View File

@ -3,7 +3,7 @@
*/
import $ from 'jquery';
import i18n from 'i18n';
import reactConfirm from 'reactstrap-confirm';
import reactConfirm from "@silverstripe/reactstrap-confirm";
$.entwine('ss', function($){
/**
@ -497,13 +497,12 @@ $.entwine('ss', function($){
'By changing the URL segment visitors will not be able to view it.'
);
if (await reactConfirm({
if (await reactConfirm(message, {
title: i18n._t(
'CMS.RemoveHomePageWarningTitle',
'Remove your home page?'
),
message,
confirmText: i18n._t(
confirmLabel: i18n._t(
'CMS.RemoveHomePageWarningLabel',
'Remove'
),

View File

@ -1,7 +1,6 @@
import $ from 'jquery';
import i18n from 'i18n';
import reactConfirm from 'reactstrap-confirm';
import { joinUrlPaths } from 'lib/urls';
import reactConfirm from "@silverstripe/reactstrap-confirm";
$.entwine('ss.tree', function($) {
$('.cms-tree').entwine({
@ -46,7 +45,7 @@ $.entwine('ss.tree', function($) {
});
const baseUrl = $('base').attr('href') || ''; // Edge17 and IE11 require absolute paths
window.location.assign(joinUrlPaths(baseUrl, urlWithParams));
window.location.assign(baseUrl + urlWithParams);
},
getTreeConfig: function() {
@ -169,13 +168,12 @@ $.entwine('ss.tree', function($) {
'By changing the URL segment visitors will not be able to view it.'
);
return await reactConfirm({
return await reactConfirm(message, {
title: i18n._t(
'CMS.RemoveHomePageWarningTitle',
'Remove your home page?'
),
message,
confirmText: i18n._t(
confirmLabel: i18n._t(
'CMS.RemoveHomePageWarningLabel',
'Remove'
),

View File

@ -1,5 +1,4 @@
import $ from 'jquery';
import { joinUrlPaths } from 'lib/urls';
/**
* Behaviour for the CMS Content Toolbar.
@ -82,7 +81,7 @@ $.entwine('ss', function ($) {
localStorage.setItem('ss.pages-view-type', viewType);
if(isContentViewInSidebar && viewType === VIEW_TYPE_LIST) {
const baseUrl = $('base').attr('href') || ''; // Edge17 and IE11 need absolute path
window.location.assign(joinUrlPaths(baseUrl, $contentView.data('url-listviewroot')));
window.location.assign(baseUrl + $contentView.data('url-listviewroot'));
return;
}

View File

@ -0,0 +1,183 @@
import $ from 'jquery';
import i18n from 'i18n';
/**
* File: CMSPageHistoryController.js
*
* Handles related interactions between the version selection form on the
* left hand side of the panel and the version displaying on the right
* hand side.
*/
$.entwine('ss', ($) => { // eslint-disable-line no-shadow
/**
* Class: #Form_VersionsForm
*
* The left hand side version selection form is the main interface for
* users to select a version to view, or to compare two versions
*/
$('#Form_VersionsForm').entwine({
/**
* Constructor
*/
onmatch() {
this._super();
},
onunmatch() {
this._super();
},
/**
* Function: submit.
*
* Submits either the compare versions form or the view single form
* display based on whether we have two or 1 option selected
*
* Todo:
* Handle coupling to admin url
*/
onsubmit(e) {
e.preventDefault();
const id = this.find(':input[name=ID]').val();
if (!id) {
return false;
}
let url = null;
let to = null;
let from = null;
const compare = (this.find(':input[name=CompareMode]').is(':checked'));
const selected = this.find('table input[type=checkbox]').filter(':checked');
if (compare) {
if (selected.length !== 2) {
return false;
}
to = selected.eq(0).val();
from = selected.eq(1).val();
url = i18n.sprintf(this.data('linkTmplCompare'), id, from, to);
} else {
to = selected.eq(0).val();
url = i18n.sprintf(this.data('linkTmplShow'), id, to);
}
$('.cms-container').loadPanel(url, '', { pjax: 'CurrentForm' });
return true;
},
});
/**
* Class: :input[name=ShowUnpublished]
*
* Used for toggling whether to show or hide unpublished versions.
*/
$('#Form_VersionsForm input[name=ShowUnpublished]').entwine({
onmatch() {
this.toggle();
this._super();
},
onunmatch() {
this._super();
},
/**
* Event: :input[name=ShowUnpublished] change
*
* Changing the show unpublished checkbox toggles whether to show
* or hide the unpublished versions. Because those rows may be being
* compared this also ensures those rows are unselected.
*/
onchange() {
this.toggle();
},
toggle() {
const self = $(this);
const unpublished = self.parents('form').find('tr[data-published=false]');
if (self.attr('checked')) {
unpublished
.removeClass('ui-helper-hidden')
.show();
} else {
unpublished
.addClass('ui-helper-hidden')
.hide()
._unselect();
}
},
});
/**
* Class: #Form_VersionsForm tr
*
* An individual row in the versions form. Selecting the row updates
* the edit form depending on whether we're showing individual version
* information or displaying comparsion.
*/
$('#Form_VersionsForm tbody tr').entwine({
/**
* Function: onclick
*
* Selects or deselects the row (if in compare mode). Will trigger
* an update of the edit form if either selected (in single mode)
* or if this is the second row selected (in compare mode)
*/
onclick() {
// compare mode
const compare = this.parents('form').find(':input[name=CompareMode]').attr('checked');
const selected = this.siblings('.active');
if (compare && this.hasClass('active')) {
this._unselect();
return;
}
if (compare) {
// check if we have already selected more than two.
if (selected.length > 1) {
// eslint-disable-next-line no-alert
alert(i18n._t('CMS.ONLYSELECTTWO', 'You can only compare two versions at this time.'));
return;
}
this._select();
// if this is the second selected then we can compare.
if (selected.length === 1) {
this.parents('form').submit();
}
return;
}
this._select();
selected._unselect();
this.parents('form').submit();
},
/**
* Function: _unselect()
*
* Unselects the row from the form selection.
*
* Using regular js to update the class rather than this.removeClass('active')
* because the latter causes the browser to continuously call
* element.compareDocumentPosition, causing the browser to hang for long
* periods of time, especially on pages with lots of versions (e.g. 100+)
*/
_unselect() {
this.get(0).classList.remove('active');
this.find(':input[type=checkbox][checked]').attr('checked', false);
},
/**
* Function: _select()
*
* Selects the currently matched row in the form selection
*/
_select() {
this.addClass('active');
this.find(':input[type=checkbox]').attr('checked', true);
},
});
});

View File

@ -2,8 +2,8 @@
import i18n from 'i18n';
import TinyMCEActionRegistrar from 'lib/TinyMCEActionRegistrar';
import React from 'react';
import { createRoot } from 'react-dom/client';
import { ApolloProvider } from '@apollo/client';
import ReactDOM from 'react-dom';
import { ApolloProvider } from 'react-apollo';
import { Provider } from 'react-redux';
import jQuery from 'jquery';
import ShortcodeSerialiser from 'lib/ShortcodeSerialiser';
@ -17,24 +17,24 @@ const plugin = {
init(editor) {
// Add "Anchor on a page" to link menu for this editor
TinyMCEActionRegistrar
.addAction(
'sslink',
{
text: i18n._t('CMS.LINKLABEL_ANCHOR', 'Anchor on a page'),
onAction: (activeEditor) => activeEditor.execCommand(commandName),
priority: 60,
},
editor.getParam('editorIdentifier'),
)
.addCommandWithUrlTest(commandName, /^\[sitetree_link.+]#[^#\]]+$/);
.addAction(
'sslink',
{
text: i18n._t('CMS.LINKLABEL_ANCHOR', 'Anchor on a page'),
onclick: (activeEditor) => activeEditor.execCommand(commandName),
priority: 60,
},
editor.settings.editorIdentifier,
)
.addCommandWithUrlTest(commandName, /^\[sitetree_link.+]#[^#\]]+$/);
// Add a command that corresponds with the above menu item
editor.addCommand(commandName, () => {
const field = jQuery(`#${editor.id}`).entwine('ss');
// Get the anchors in the current field and save them as props for AnchorSelectorField
const currentPageID = Number(jQuery('#Form_EditForm_ID').val() || 0);
const validTargets = jQuery(editor.getBody())
.find('[id],[name]')
const validTargets = editor
.$('[id],[name]', editor.getBody())
.toArray()
.map((element) => element.id || element.name);
ss.store.dispatch(updatedCurrentField(currentPageID, validTargets, editor.id));
@ -69,8 +69,6 @@ jQuery.entwine('ss', ($) => {
* Assumes that $('.insert-link__dialog-wrapper').entwine({}); is defined for shared functions
*/
$(`#${modalId}`).entwine({
ReactRoot: null,
renderModal(isOpen) {
const store = ss.store;
const client = ss.apolloClient;
@ -81,12 +79,7 @@ jQuery.entwine('ss', ($) => {
const currentPageID = Number($('#Form_EditForm_ID').val() || 0);
// create/update the react component
let root = this.getReactRoot();
if (!root) {
root = createRoot(this[0]);
this.setReactRoot(root);
}
root.render(
ReactDOM.render(
<ApolloProvider client={client}>
<Provider store={store}>
<InsertLinkInternalModal
@ -102,7 +95,8 @@ jQuery.entwine('ss', ($) => {
currentPageID={currentPageID}
/>
</Provider>
</ApolloProvider>
</ApolloProvider>,
this[0]
);
},

View File

@ -2,8 +2,8 @@
import i18n from 'i18n';
import TinyMCEActionRegistrar from 'lib/TinyMCEActionRegistrar';
import React from 'react';
import { createRoot } from 'react-dom/client';
import { ApolloProvider } from '@apollo/client';
import ReactDOM from 'react-dom';
import { ApolloProvider } from 'react-apollo';
import { Provider } from 'react-redux';
import jQuery from 'jquery';
import ShortcodeSerialiser from 'lib/ShortcodeSerialiser';
@ -16,16 +16,16 @@ const plugin = {
init(editor) {
// Add "Page on this site" to link menu for this editor
TinyMCEActionRegistrar
.addAction(
'sslink',
{
text: i18n._t('CMS.LINKLABEL_PAGE', 'Page on this site'),
onAction: (activeEditor) => activeEditor.execCommand(commandName),
priority: 90,
},
editor.getParam('editorIdentifier'),
)
.addCommandWithUrlTest(commandName, /^\[sitetree_link.+]$/);
.addAction(
'sslink',
{
text: i18n._t('CMS.LINKLABEL_PAGE', 'Page on this site'),
onclick: (activeEditor) => activeEditor.execCommand(commandName),
priority: 90,
},
editor.settings.editorIdentifier,
)
.addCommandWithUrlTest(commandName, /^\[sitetree_link.+]$/);
// Add a command that corresponds with the above menu item
editor.addCommand(commandName, () => {
@ -61,8 +61,6 @@ jQuery.entwine('ss', ($) => {
* Assumes that $('.insert-link__dialog-wrapper').entwine({}); is defined for shared functions
*/
$(`#${modalId}`).entwine({
ReactRoot: null,
renderModal(isOpen) {
const store = ss.store;
const client = ss.apolloClient;
@ -72,12 +70,7 @@ jQuery.entwine('ss', ($) => {
const requireLinkText = this.getRequireLinkText();
// create/update the react component
let root = this.getReactRoot();
if (!root) {
root = createRoot(this[0]);
this.setReactRoot(root);
}
root.render(
ReactDOM.render(
<ApolloProvider client={client}>
<Provider store={store}>
<InsertLinkInternalModal
@ -92,7 +85,8 @@ jQuery.entwine('ss', ($) => {
requireLinkText={requireLinkText}
/>
</Provider>
</ApolloProvider>
</ApolloProvider>,
this[0]
);
},

View File

@ -1,4 +1,4 @@
import { graphql } from '@apollo/client/react/hoc';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
// GraphQL query for retrieving the version history of a specific page. The
@ -88,11 +88,11 @@ const config = {
versions: {
...versions,
goToPage(page) {
refetch({
refetch({
offset: ((page || 1) - 1) * limit,
limit,
page_id: recordId,
});
});
}
},
},

View File

@ -1,4 +1,4 @@
import { graphql } from '@apollo/client/react/hoc';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
const mutation = gql`

View File

@ -1,4 +1,4 @@
import { graphql } from '@apollo/client/react/hoc';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
const mutation = gql`

View File

@ -7,5 +7,3 @@
@import "legacy/CMSMain";
@import "legacy/ReportAdmin";
@import "../components/AnchorSelectorField/AnchorSelectorField";

View File

@ -30,6 +30,31 @@
}
}
.CMSPageHistoryController {
overflow: hidden; // Fixes weird bug for double scroll in history area on browser resize
ins {
background-color: #DFD;
padding: 2px;
text-decoration: none;
}
del {
background-color: #FDD;
padding: 2px;
color: darken(#FDD, 30%);
}
.htmleditorfield.readonly img {
max-width: 100%;
height: auto;
}
.cms-content-tools.collapsed {
overflow: hidden;
}
}
/** --------------------------------------------
* Tree View (collapsed for sidebar)
* -------------------------------------------- */

View File

@ -4,7 +4,6 @@ namespace SilverStripe\CMS\BatchActions;
use SilverStripe\ORM\SS_List;
use SilverStripe\Admin\CMSBatchAction;
use SilverStripe\Control\HTTPResponse;
/**
* Delete items batch action.
@ -16,7 +15,7 @@ class CMSBatchAction_Archive extends CMSBatchAction
return _t(__CLASS__ . '.TITLE', 'Unpublish and archive');
}
public function run(SS_List $pages): HTTPResponse
public function run(SS_List $pages)
{
return $this->batchaction(
$pages,

View File

@ -3,7 +3,6 @@
namespace SilverStripe\CMS\BatchActions;
use SilverStripe\Admin\CMSBatchAction;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\ORM\SS_List;
/**
@ -16,7 +15,7 @@ class CMSBatchAction_Publish extends CMSBatchAction
return _t(__CLASS__ . '.PUBLISH_PAGES', 'Publish');
}
public function run(SS_List $pages): HTTPResponse
public function run(SS_List $pages)
{
return $this->batchaction(
$pages,

View File

@ -4,7 +4,6 @@ namespace SilverStripe\CMS\BatchActions;
use SilverStripe\Admin\CMSBatchAction;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\SS_List;
use SilverStripe\Versioned\Versioned;
@ -21,7 +20,7 @@ class CMSBatchAction_Restore extends CMSBatchAction
return _t(__CLASS__ . '.RESTORE', 'Restore');
}
public function run(SS_List $pages): HTTPResponse
public function run(SS_List $pages)
{
// Sort pages by depth
$pageArray = $pages->toArray();

View File

@ -3,7 +3,6 @@
namespace SilverStripe\CMS\BatchActions;
use SilverStripe\Admin\CMSBatchAction;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\ORM\SS_List;
/**
@ -16,7 +15,7 @@ class CMSBatchAction_Unpublish extends CMSBatchAction
return _t(__CLASS__ . '.UNPUBLISH_PAGES', 'Unpublish');
}
public function run(SS_List $pages): HTTPResponse
public function run(SS_List $pages)
{
return $this->batchaction(
$pages,

View File

@ -2,13 +2,13 @@
namespace SilverStripe\CMS\Controllers;
use SilverStripe\Dev\Deprecation;
use InvalidArgumentException;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Admin\AdminRootController;
use SilverStripe\Admin\CMSBatchActionHandler;
use SilverStripe\Admin\LeftAndMain;
use SilverStripe\Admin\LeftAndMainFormRequestHandler;
use SilverStripe\Admin\Navigator\SilverStripeNavigator;
use SilverStripe\CMS\BatchActions\CMSBatchAction_Archive;
use SilverStripe\CMS\BatchActions\CMSBatchAction_Publish;
use SilverStripe\CMS\BatchActions\CMSBatchAction_Restore;
@ -23,7 +23,6 @@ use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\PjaxResponseNegotiator;
use SilverStripe\Core\Cache\MemberCacheFlusher;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
@ -70,9 +69,10 @@ use SilverStripe\SiteConfig\SiteConfig;
use SilverStripe\Versioned\ChangeSet;
use SilverStripe\Versioned\ChangeSetItem;
use SilverStripe\Versioned\Versioned;
use SilverStripe\VersionedAdmin\Controllers\CMSPageHistoryViewerController;
use SilverStripe\View\ArrayData;
use SilverStripe\View\Requirements;
use Translatable;
use SilverStripe\VersionedAdmin\Controllers\CMSPageHistoryViewerController;
/**
* The main "content" area of the CMS.
@ -80,6 +80,8 @@ use SilverStripe\View\Requirements;
* This class creates a 2-frame layout - left-tree and right-form - to sit beneath the main
* admin menu.
*
* @todo Create some base classes to contain the generic functionality that will be replicated.
*
* @mixin LeftAndMainPageIconsExtension
*/
class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionProvider, Flushable, MemberCacheFlusher
@ -105,6 +107,12 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
private static $tree_class = SiteTree::class;
/**
* @deprecated 1.13.0 Do not use this options.
* @config
*/
private static $subitem_class = Member::class;
private static $session_namespace = self::class;
private static $required_permission_codes = 'CMS_ACCESS_CMSMain';
@ -180,13 +188,19 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
protected function init()
{
// set reading lang
if (SiteTree::has_extension('Translatable') && !$this->getRequest()->isAjax()) {
Translatable::choose_site_locale(array_keys(Translatable::get_existing_content_languages(SiteTree::class) ?? []));
}
parent::init();
Requirements::javascript('silverstripe/cms: client/dist/js/bundle.js');
Requirements::javascript('silverstripe/cms: client/dist/js/SilverStripeNavigator.js');
Requirements::css('silverstripe/cms: client/dist/styles/bundle.css');
Requirements::customCSS($this->generatePageIconsCss(), self::PAGE_ICONS_ID);
Requirements::add_i18n_javascript('silverstripe/cms: client/lang', false);
Requirements::add_i18n_javascript('silverstripe/cms: client/lang', false, true);
CMSBatchActionHandler::register('restore', CMSBatchAction_Restore::class);
CMSBatchActionHandler::register('archive', CMSBatchAction_Archive::class);
@ -194,7 +208,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
CMSBatchActionHandler::register('publish', CMSBatchAction_Publish::class);
}
public function index(HTTPRequest $request): HTTPResponse
public function index($request)
{
// In case we're not showing a specific record, explicitly remove any session state,
// to avoid it being highlighted in the tree, and causing an edit form to show.
@ -205,7 +219,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return parent::index($request);
}
public function getResponseNegotiator(): PjaxResponseNegotiator
public function getResponseNegotiator()
{
$negotiator = parent::getResponseNegotiator();
@ -400,13 +414,10 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
public function LinkPageHistory()
{
$controller = Injector::inst()->get(CMSPageHistoryViewerController::class);
if (($id = $this->currentPageID()) && $controller) {
if ($controller) {
return $this->LinkWithSearch(
Controller::join_links($controller->Link('show'), $id)
);
}
if ($id = $this->currentPageID()) {
return $this->LinkWithSearch(
Controller::join_links(CMSPageHistoryViewerController::singleton()->Link('show'), $id)
);
} else {
return null;
}
@ -653,8 +664,11 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
/**
* Get a subtree underneath the request param 'ID'.
* If ID = 0, then get the whole tree.
*
* @param HTTPRequest $request
* @return string
*/
public function getsubtree(HTTPRequest $request): HTTPResponse
public function getsubtree($request)
{
$html = $this->getSiteTreeFor(
$this->config()->get('tree_class'),
@ -669,7 +683,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$html = preg_replace('/^[\s\t\r\n]*<ul[^>]*>/', '', $html ?? '');
$html = preg_replace('/<\/ul[^>]*>[\s\t\r\n]*$/', '', $html ?? '');
return $this->getResponse()->setBody($html);
return $html;
}
/**
@ -677,8 +691,11 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
* Similar to {@link getsubtree()}, but doesn't enforce loading
* all children with the node. Useful to refresh views after
* state modifications, e.g. saving a form.
*
* @param HTTPRequest $request
* @return HTTPResponse
*/
public function updatetreenodes(HTTPRequest $request): HTTPResponse
public function updatetreenodes($request)
{
$data = [];
$ids = explode(',', $request->getVar('ids') ?? '');
@ -700,6 +717,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$markingSet->markUnexpanded($record);
// Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
// TODO: These methods should really be in hierarchy - for a start it assumes Sort exists
$prev = null;
$className = $this->config()->get('tree_class');
@ -745,15 +763,17 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
* - 'SiblingIDs': Array of all sibling nodes to the moved node (incl. the node itself).
* In case of a 'ParentID' change, relates to the new siblings under the new parent.
*
* @param HTTPRequest $request
* @return HTTPResponse JSON string with a
* @throws HTTPResponse_Exception
*/
public function savetreenode(HTTPRequest $request): HTTPResponse
public function savetreenode($request)
{
if (!SecurityToken::inst()->checkRequest($request)) {
$this->httpError(400);
return $this->httpError(400);
}
if (!$this->CanOrganiseSitetree()) {
$this->httpError(
return $this->httpError(
403,
_t(
__CLASS__.'.CANT_REORGANISE',
@ -766,14 +786,14 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$id = $request->requestVar('ID');
$parentID = $request->requestVar('ParentID');
if (!is_numeric($id) || !is_numeric($parentID)) {
$this->httpError(400);
return $this->httpError(400);
}
// Check record exists in the DB
/** @var SiteTree $node */
$node = DataObject::get_by_id($className, $id);
if (!$node) {
$this->httpError(
return $this->httpError(
500,
_t(
__CLASS__.'.PLEASESAVE',
@ -785,7 +805,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// Check top level permissions
$root = $node->getParentType();
if (($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()) {
$this->httpError(
return $this->httpError(
403,
_t(
__CLASS__.'.CANT_REORGANISE',
@ -980,6 +1000,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
);
// Create the form
/** @skipUpgrade */
$form = Form::create(
$this,
'SearchForm',
@ -1016,7 +1037,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $pageTypes;
}
public function doSearch(array $data, Form $form): HTTPResponse
public function doSearch($data, $form)
{
return $this->getsubtree($this->getRequest());
}
@ -1038,6 +1059,10 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
->Link;
}
/**
* @param bool $unlinked
* @return ArrayList
*/
public function Breadcrumbs($unlinked = false)
{
$items = new ArrayList();
@ -1289,6 +1314,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
if (!$id) {
$id = $this->currentPageID();
}
/** @var SiteTree $record */
$record = $this->getRecord($id);
// Check parent form can be generated
@ -1326,9 +1352,8 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
// Added in-line to the form, but plucked into different view by LeftAndMain.Preview.js upon load
if (($record instanceof CMSPreviewable || $record->has_extension(CMSPreviewable::class))
&& !$fields->fieldByName('SilverStripeNavigator')
) {
/** @skipUpgrade */
if ($record instanceof CMSPreviewable && !$fields->fieldByName('SilverStripeNavigator')) {
$navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
$navField->setAllowHTML(true);
$fields->push($navField);
@ -1364,6 +1389,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
}
// TODO Can't merge $FormAttributes in template at the moment
$form->addExtraClass('center ' . $this->BaseCSSClasses());
// Set validation exemptions for specific actions
$form->setValidationExemptActions([
@ -1378,7 +1404,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
]);
// Announce the capability so the frontend can decide whether to allow preview or not.
if ($record instanceof CMSPreviewable || $record->has_extension(CMSPreviewable::class)) {
if ($record instanceof CMSPreviewable) {
$form->addExtraClass('cms-previewable');
}
$form->addExtraClass('fill-height flexbox-area-grow');
@ -1534,14 +1560,17 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
/**
* Callback to request the list of page types allowed under a given page instance.
* Provides a slower but more precise response over SiteTreeHints
*
* @param HTTPRequest $request
* @return HTTPResponse
*/
public function childfilter(HTTPRequest $request): HTTPResponse
public function childfilter($request)
{
// Check valid parent specified
$parentID = $request->requestVar('ParentID');
$parent = SiteTree::get()->byID($parentID);
if (!$parent || !$parent->exists()) {
$this->httpError(404);
return $this->httpError(404);
}
// Build hints specific to this class
@ -1634,6 +1663,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
$gridField = GridField::create('Page', 'Pages', $list, $gridFieldConfig);
$gridField->setAttribute('cms-loading-ignore-url-params', true);
/** @var GridFieldDataColumns $columns */
$columns = $gridField->getConfig()->getComponentByType(GridFieldDataColumns::class);
// Don't allow navigating into children nodes on filtered lists
@ -1642,6 +1672,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
'i18n_singular_name' => _t('SilverStripe\\CMS\\Model\\SiteTree.PAGETYPE', 'Page Type'),
'LastEdited' => _t('SilverStripe\\CMS\\Model\\SiteTree.LASTUPDATED', 'Last Updated'),
];
/** @var GridFieldSortableHeader $sortableHeader */
$sortableHeader = $gridField->getConfig()->getComponentByType(GridFieldSortableHeader::class);
$sortableHeader->setFieldSorting(['getTreeTitle' => 'Title']);
$gridField->getState()->ParentID = $parentID;
@ -1726,9 +1757,12 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
/**
* Save and Publish page handler
*
* @param array $data
* @param Form $form
* @return HTTPResponse
* @throws HTTPResponse_Exception
*/
public function save(array $data, Form $form): HTTPResponse
public function save($data, $form)
{
$className = $this->config()->get('tree_class');
@ -1757,6 +1791,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return Security::permissionFailure($this);
}
// TODO Coupling to SiteTree
$record->HasBrokenLink = 0;
$record->HasBrokenFile = 0;
@ -1870,9 +1905,12 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
*
* @uses SiteTree->doRevertToLive()
*
* @param array $data
* @param Form $form
* @return HTTPResponse
* @throws HTTPResponse_Exception
*/
public function revert(array $data, Form $form): HTTPResponse
public function revert($data, $form)
{
if (!isset($data['ID'])) {
throw new HTTPResponse_Exception("Please pass an ID in the form content", 400);
@ -1884,6 +1922,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
throw new HTTPResponse_Exception("SiteTree #$id not found", 400);
}
/** @var SiteTree $record */
$table = DataObject::singleton(SiteTree::class)->baseTable();
$liveTable = DataObject::singleton(SiteTree::class)->stageTable($table, Versioned::LIVE);
$record = Versioned::get_one_by_stage(SiteTree::class, Versioned::LIVE, [
@ -1919,9 +1958,12 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
*
* @see deletefromlive()
*
* @param array $data
* @param Form $form
* @return HTTPResponse
* @throws HTTPResponse_Exception
*/
public function delete(array $data, Form $form): HTTPResponse
public function delete($data, $form)
{
$id = $data['ID'];
$record = SiteTree::get()->byID($id);
@ -1951,11 +1993,15 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
/**
* Delete this page from both live and stage
*
* @param array $data
* @param Form $form
* @return HTTPResponse
* @throws HTTPResponse_Exception
*/
public function archive(array $data, Form $form): HTTPResponse
public function archive($data, $form)
{
$id = $data['ID'];
/** @var SiteTree $record */
$record = SiteTree::get()->byID($id);
if (!$record || !$record->exists()) {
throw new HTTPResponse_Exception("Bad record ID #$id", 404);
@ -1980,14 +2026,14 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $this->getResponseNegotiator()->respond($this->getRequest());
}
public function publish(array $data, Form $form): HTTPResponse
public function publish($data, $form)
{
$data['publish'] = '1';
return $this->save($data, $form);
}
public function unpublish(array $data, Form $form): HTTPResponse
public function unpublish($data, $form)
{
$className = $this->config()->get('tree_class');
/** @var SiteTree $record */
@ -2123,15 +2169,93 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
/**
* Restore a completely deleted page from the SiteTree_versions table.
* @deprecated 4.12.0 Use custom logic instead
* @param $request
* @return HTTPResponse|string|void
*/
public function restore(array $data, Form $form): HTTPResponse
public function publishall($request)
{
Deprecation::notice('4.12.0', 'Use custom logic instead');
if (!Permission::check('ADMIN')) {
return Security::permissionFailure($this);
}
Environment::increaseTimeLimitTo();
Environment::increaseMemoryLimitTo();
$response = "";
if (isset($this->requestParams['confirm'])) {
// Protect against CSRF on destructive action
if (!SecurityToken::inst()->checkRequest($request)) {
return $this->httpError(400);
}
$start = 0;
$pages = SiteTree::get()->limit("$start,30");
$count = 0;
while ($pages) {
/** @var SiteTree $page */
foreach ($pages as $page) {
if ($page && !$page->canPublish()) {
return Security::permissionFailure($this);
}
$page->publishRecursive();
$page->destroy();
unset($page);
$count++;
$response .= "<li>$count</li>";
}
if ($pages->count() > 29) {
$start += 30;
$pages = SiteTree::get()->limit("$start,30");
} else {
break;
}
}
$response .= _t(__CLASS__ . '.PUBPAGES', "Done: Published {count} pages", ['count' => $count]);
} else {
$token = SecurityToken::inst();
$fields = new FieldList();
$token->updateFieldSet($fields);
$tokenField = $fields->first();
$tokenHtml = ($tokenField) ? $tokenField->FieldHolder() : '';
$publishAllDescription = _t(
__CLASS__ . '.PUBALLFUN2',
'Pressing this button will do the equivalent of going to every page and pressing "publish". '
. 'It\'s intended to be used after there have been massive edits of the content, such as when '
. 'the site was first built. '
. 'For large websites, this task might not be able to run through to completion. '
. 'In this case, we recommend talking to your developers to create a custom task'
);
$response .= '<h1>' . _t(__CLASS__ . '.PUBALLFUN', '"Publish All" functionality') . '</h1>
<p>' . $publishAllDescription . '</p>
<form method="post" action="publishall">
<input type="submit" name="confirm" value="'
. _t(__CLASS__ . '.PUBALLCONFIRM', "Please publish every page in the site, copying content stage to live", 'Confirmation button') .'" />'
. $tokenHtml .
'</form>';
}
return $response;
}
/**
* Restore a completely deleted page from the SiteTree_versions table.
*
* @param array $data
* @param Form $form
* @return HTTPResponse
*/
public function restore($data, $form)
{
if (!isset($data['ID']) || !is_numeric($data['ID'])) {
return new HTTPResponse("Please pass an ID in the form content", 400);
}
$id = (int)$data['ID'];
/** @var SiteTree $restoredPage */
$restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
if (!$restoredPage) {
return new HTTPResponse("SiteTree #$id not found", 400);
@ -2151,7 +2275,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $this->getResponseNegotiator()->respond($this->getRequest());
}
public function duplicate(HTTPRequest $request): HTTPResponse
public function duplicate($request)
{
// Protect against CSRF on destructive action
if (!SecurityToken::inst()->checkRequest($request)) {
@ -2159,6 +2283,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
if (($id = $this->urlParams['ID']) && is_numeric($id)) {
/** @var SiteTree $page */
$page = SiteTree::get()->byID($id);
if ($page && !$page->canCreate(null, ['Parent' => $page->Parent()])) {
return Security::permissionFailure($this);
@ -2167,6 +2292,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
throw new HTTPResponse_Exception("Bad record ID #$id", 404);
}
/** @var SiteTree $newPage */
$newPage = $page->duplicate();
// ParentID can be hard-set in the URL. This is useful for pages with multiple parents
@ -2193,14 +2319,15 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return new HTTPResponse("CMSMain::duplicate() Bad ID: '$id'", 400);
}
public function duplicatewithchildren(HTTPRequest $request): HTTPResponse
public function duplicatewithchildren($request)
{
// Protect against CSRF on destructive action
if (!SecurityToken::inst()->checkRequest($request)) {
$this->httpError(400);
return $this->httpError(400);
}
Environment::increaseTimeLimitTo();
if (($id = $this->urlParams['ID']) && is_numeric($id)) {
/** @var SiteTree $page */
$page = SiteTree::get()->byID($id);
if ($page && !$page->canCreate(null, ['Parent' => $page->Parent()])) {
return Security::permissionFailure($this);
@ -2209,6 +2336,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
throw new HTTPResponse_Exception("Bad record ID #$id", 404);
}
/** @var SiteTree $newPage */
$newPage = $page->duplicateWithChildren();
$this->getResponse()->addHeader(

View File

@ -132,6 +132,10 @@ class CMSPageAddController extends CMSPageEditController
]);
});
// TODO Re-enable search once it allows for HTML title display,
// see http://open.silverstripe.org/ticket/7455
// $parentField->setShowSearch(true);
$parentModeField->addExtraClass('parent-mode');
// CMSMain->currentPageID() automatically sets the homepage,
@ -188,7 +192,12 @@ class CMSPageAddController extends CMSPageEditController
return $form;
}
public function doAdd(array $data, Form $form): HTTPResponse
/**
* @param array $data
* @param Form $form
* @return HTTPResponse
*/
public function doAdd($data, $form)
{
$className = isset($data['PageType']) ? $data['PageType'] : "Page";
$parentID = isset($data['ParentID']) ? (int)$data['ParentID'] : 0;
@ -232,7 +241,7 @@ class CMSPageAddController extends CMSPageEditController
return $this->redirect(Controller::join_links($editController->Link('show'), $record->ID));
}
public function doCancel(array $data, Form $form): HTTPResponse
public function doCancel($data, $form)
{
return $this->redirect(CMSMain::singleton()->Link());
}

View File

@ -53,27 +53,30 @@ class CMSPageEditController extends CMSMain
/**
* Action handler for adding pages to a campaign
*
* @param array $data
* @param Form $form
* @return DBHTMLText|HTTPResponse
*/
public function addtocampaign(array $data, Form $form): HTTPResponse
public function addtocampaign($data, $form)
{
$id = $data['ID'];
$record = \Page::get()->byID($id);
$handler = AddToCampaignHandler::create($this, $record);
$response = $handler->addToCampaign($record, $data);
$message = $response->getBody();
if (empty($message)) {
return $response;
$results = $handler->addToCampaign($record, $data);
if (is_null($results)) {
return null;
}
if ($this->getSchemaRequested()) {
// Send extra "message" data with schema response
$extraData = ['message' => $message];
$extraData = ['message' => $results];
$schemaId = Controller::join_links($this->Link('schema/AddToCampaignForm'), $id);
return $this->getSchemaResponse($schemaId, $form, null, $extraData);
}
return $response;
return $results;
}
/**

View File

@ -0,0 +1,480 @@
<?php
namespace SilverStripe\CMS\Controllers;
use SilverStripe\Dev\Deprecation;
use SilverStripe\Admin\LeftAndMainFormRequestHandler;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\HTMLReadonlyField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\Tab;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\Versioned\Versioned;
use SilverStripe\Security\Security;
use SilverStripe\View\ArrayData;
use SilverStripe\View\ViewableData;
/**
* Legacy CMS History controller. This functionality has been moved to the `silverstripe/versioned-admin` module and
* this class will be removed completly in SilverStripe 5.0.0.
* @deprecated 4.3.0 Use silverstripe/versioned-admin instead
*/
class CMSPageHistoryController extends CMSMain
{
private static $url_segment = 'pages/history';
private static $url_rule = '/$Action/$ID/$VersionID/$OtherVersionID';
private static $url_priority = 42;
private static $menu_title = 'History';
private static $required_permission_codes = 'CMS_ACCESS_CMSMain';
private static $allowed_actions = [
'EditForm',
'VersionsForm',
'CompareVersionsForm',
'show',
'compare'
];
private static $url_handlers = [
'$Action/$ID/$VersionID/$OtherVersionID' => 'handleAction',
'EditForm/$ID/$VersionID' => 'EditForm',
];
/**
* Current version ID for this request. Can be 0 for latest version
*
* @var int
*/
protected $versionID = null;
public function __construct()
{
parent::__construct();
Deprecation::withNoReplacement(function () {
Deprecation::notice('4.3.0', 'Use silverstripe/versioned-admin instead', Deprecation::SCOPE_CLASS);
});
}
public function getResponseNegotiator()
{
$negotiator = parent::getResponseNegotiator();
$negotiator->setCallback('CurrentForm', function () {
$form = $this->getEditForm();
if ($form) {
return $form->forTemplate();
}
return $this->renderWith($this->getTemplatesWithSuffix('_Content'));
});
$negotiator->setCallback('default', function () {
return $this->renderWith($this->getViewer('show'));
});
return $negotiator;
}
/**
* @param HTTPRequest $request
* @return HTTPResponse
*/
public function show($request)
{
// Record id and version for this request
$id = $request->param('ID');
$this->setCurrentPageID($id);
$versionID = $request->param('VersionID');
$this->setVersionID($versionID);
// Show id
$form = $this->getEditForm();
$negotiator = $this->getResponseNegotiator();
$negotiator->setCallback('CurrentForm', function () use ($form) {
return $form
? $form->forTemplate()
: $this->renderWith($this->getTemplatesWithSuffix('_Content'));
});
$negotiator->setCallback('default', function () use ($form) {
return $this
->customise(['EditForm' => $form])
->renderWith($this->getViewer('show'));
});
return $negotiator->respond($request);
}
/**
* @param HTTPRequest $request
* @return HTTPResponse
*/
public function compare($request)
{
$form = $this->CompareVersionsForm(
$request->param('VersionID'),
$request->param('OtherVersionID')
);
$negotiator = $this->getResponseNegotiator();
$negotiator->setCallback('CurrentForm', function () use ($form) {
return $form ? $form->forTemplate() : $this->renderWith($this->getTemplatesWithSuffix('_Content'));
});
$negotiator->setCallback('default', function () use ($form) {
return $this->customise(['EditForm' => $form])->renderWith($this->getViewer('show'));
});
return $negotiator->respond($request);
}
public function getSilverStripeNavigator()
{
$record = $this->getRecord($this->currentPageID(), $this->getRequest()->param('VersionID'));
if ($record) {
$navigator = new SilverStripeNavigator($record);
return $navigator->renderWith($this->getTemplatesWithSuffix('_SilverStripeNavigator'));
} else {
return false;
}
}
/**
* @param HTTPRequest $request
* @return Form
*/
public function EditForm($request = null)
{
if ($request) {
// Validate VersionID is present
$versionID = $request->param('VersionID');
if (!isset($versionID)) {
$this->httpError(400);
return null;
}
$this->setVersionID($versionID);
}
return parent::EditForm($request);
}
/**
* Returns the read only version of the edit form. Detaches all {@link FormAction}
* instances attached since only action relates to revert.
*
* Permission checking is done at the {@link CMSMain::getEditForm()} level.
*
* @param int $id ID of the record to show
* @param array $fields optional
* @param int $versionID
* @param int $compareID Compare mode
*
* @return Form
*/
public function getEditForm($id = null, $fields = null, $versionID = null, $compareID = null)
{
if (!$id) {
$id = $this->currentPageID();
}
if (!$versionID) {
$versionID = $this->getVersionID();
}
$record = $this->getRecord($id, $versionID);
if (!$record) {
return $this->EmptyForm();
}
// Refresh version ID
$versionID = $record->Version;
$this->setVersionID($versionID);
// Get edit form
$form = parent::getEditForm($record, $record->getCMSFields());
// Respect permission failures from parent implementation
if (!($form instanceof Form)) {
return $form;
}
// TODO: move to the SilverStripeNavigator structure so the new preview can pick it up.
//$nav = new SilverStripeNavigatorItem_ArchiveLink($record);
$actions = new FieldList(
$revert = FormAction::create(
'doRollback',
_t('SilverStripe\\CMS\\Controllers\\CMSPageHistoryController.REVERTTOTHISVERSION', 'Revert to this version')
)
->setUseButtonTag(true)
->addExtraClass('btn-warning font-icon-back-in-time')
);
$actions->setForm($form);
$form->setActions($actions);
$fields = $form->Fields();
$fields->removeByName("Status");
$fields->push(new HiddenField("ID"));
$fields->push(new HiddenField("Version"));
$fields = $fields->makeReadonly();
if ($compareID) {
$link = Controller::join_links(
$this->Link('show'),
$id
);
$view = _t(__CLASS__ . '.VIEW', "view");
$message = _t(
__CLASS__ . '.COMPARINGVERSION',
"Comparing versions {version1} and {version2}.",
[
'version1' => sprintf('%s (<a href="%s">%s</a>)', $versionID, Controller::join_links($link, $versionID), $view),
'version2' => sprintf('%s (<a href="%s">%s</a>)', $compareID, Controller::join_links($link, $compareID), $view)
]
);
$revert->setReadonly(true);
} else {
if ($record->isLatestVersion()) {
$message = _t(__CLASS__ . '.VIEWINGLATEST', 'Currently viewing the latest version.');
} else {
$message = _t(
__CLASS__ . '.VIEWINGVERSION',
"Currently viewing version {version}.",
['version' => $versionID]
);
}
}
/** @var Tab $mainTab */
$mainTab = $fields->fieldByName('Root.Main');
$mainTab->unshift(
LiteralField::create('CurrentlyViewingMessage', ArrayData::create([
'Content' => DBField::create_field('HTMLFragment', $message),
'Classes' => 'alert alert-info'
])->renderWith($this->getTemplatesWithSuffix('_notice')))
);
$form->setFields($fields->makeReadonly());
$form->loadDataFrom([
"ID" => $id,
"Version" => $versionID,
]);
if ($record->isLatestVersion()) {
$revert->setReadonly(true);
}
$form->removeExtraClass('cms-content');
// History form has both ID and VersionID as suffixes
$form->setRequestHandler(
LeftAndMainFormRequestHandler::create($form, [$id, $versionID])
);
return $form;
}
/**
* Version select form. Main interface between selecting versions to view
* and comparing multiple versions.
*
* Because we can reload the page directly to a compare view (history/compare/1/2/3)
* this form has to adapt to those parameters as well.
*
* @return Form
*/
public function VersionsForm()
{
$id = $this->currentPageID();
$page = $this->getRecord($id);
$versionsHtml = '';
$action = $this->getRequest()->param('Action');
$versionID = $this->getRequest()->param('VersionID');
$otherVersionID = $this->getRequest()->param('OtherVersionID');
$showUnpublishedChecked = 0;
$compareModeChecked = ($action == "compare");
if ($page) {
$versions = $page->Versions();
$versionID = (!$versionID) ? $page->Version : $versionID;
if ($versions) {
foreach ($versions as $k => $version) {
$active = false;
if ($version->Version == $versionID || $version->Version == $otherVersionID) {
$active = true;
if (!$version->WasPublished) {
$showUnpublishedChecked = 1;
}
}
$version->Active = ($active);
}
}
$vd = new ViewableData();
$versionsHtml = $vd->customise([
'Versions' => $versions
])->renderWith($this->getTemplatesWithSuffix('_versions'));
}
$fields = new FieldList(
new CheckboxField(
'ShowUnpublished',
_t('SilverStripe\\CMS\\Controllers\\CMSPageHistoryController.SHOWUNPUBLISHED', 'Show unpublished versions'),
$showUnpublishedChecked
),
new CheckboxField(
'CompareMode',
_t('SilverStripe\\CMS\\Controllers\\CMSPageHistoryController.COMPAREMODE', 'Compare mode (select two)'),
$compareModeChecked
),
new LiteralField('VersionsHtml', $versionsHtml),
$hiddenID = new HiddenField('ID', false, "")
);
$form = Form::create(
$this,
'VersionsForm',
$fields,
new FieldList()
)->setHTMLID('Form_VersionsForm');
$form->loadDataFrom($this->getRequest()->requestVars());
$hiddenID->setValue($id);
$form->unsetValidator();
$form
->addExtraClass('cms-versions-form') // placeholder, necessary for $.metadata() to work
->setAttribute('data-link-tmpl-compare', Controller::join_links($this->Link('compare'), '%s', '%s', '%s'))
->setAttribute('data-link-tmpl-show', Controller::join_links($this->Link('show'), '%s', '%s'));
return $form;
}
/**
* @param int $versionID
* @param int $otherVersionID
* @return mixed
*/
public function CompareVersionsForm($versionID, $otherVersionID)
{
if ($versionID > $otherVersionID) {
$toVersion = $versionID;
$fromVersion = $otherVersionID;
} else {
$toVersion = $otherVersionID;
$fromVersion = $versionID;
}
if (!$toVersion || !$fromVersion) {
return null;
}
$id = $this->currentPageID();
/** @var SiteTree $page */
$page = SiteTree::get()->byID($id);
$record = null;
if ($page && $page->exists()) {
if (!$page->canView()) {
return Security::permissionFailure($this);
}
$record = $page->compareVersions($fromVersion, $toVersion);
}
$fromVersionRecord = Versioned::get_version(SiteTree::class, $id, $fromVersion);
$toVersionRecord = Versioned::get_version(SiteTree::class, $id, $toVersion);
if (!$fromVersionRecord) {
throw new \Exception("Can't find version $fromVersion of page $id");
}
if (!$toVersionRecord) {
throw new \Exception("Can't find version $toVersion of page $id");
}
if (!$record) {
return null;
}
$form = $this->getEditForm($id, null, $fromVersion, $toVersion);
$form->setActions(new FieldList());
$form->addExtraClass('compare');
$form->loadDataFrom($record);
$form->loadDataFrom([
"ID" => $id,
"Version" => $fromVersion,
]);
// Comparison views shouldn't be editable.
// As the comparison output is HTML and not valid values for the various field types
$readonlyFields = $this->transformReadonly($form->Fields());
$form->setFields($readonlyFields);
return $form;
}
/**
* Replace all data fields with HTML readonly fields to display diff
*
* @param FieldList $fields
* @return FieldList
*/
public function transformReadonly(FieldList $fields)
{
foreach ($fields->dataFields() as $field) {
if ($field instanceof HiddenField) {
continue;
}
$newField = $field->castedCopy(HTMLReadonlyField::class);
$fields->replaceField($field->getName(), $newField);
}
return $fields;
}
/**
* Set current version ID
*
* @param int $versionID
* @return $this
*/
public function setVersionID($versionID)
{
$this->versionID = $versionID;
return $this;
}
/**
* Get current version ID
*
* @return int
*/
public function getVersionID()
{
return $this->versionID;
}
public function getTabIdentifier()
{
return 'history';
}
}

View File

@ -2,7 +2,6 @@
namespace SilverStripe\CMS\Controllers;
use SilverStripe\Admin\Navigator\SilverStripeNavigator;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
@ -30,6 +29,7 @@ use SilverStripe\View\ArrayData;
use SilverStripe\View\Parsers\URLSegmentFilter;
use SilverStripe\View\Requirements;
use SilverStripe\View\SSViewer;
use Translatable;
/**
* The most common kind of controller; effectively a controller linked to a {@link DataObject}.
@ -45,12 +45,12 @@ use SilverStripe\View\SSViewer;
* Subclasses of ContentController are generally instantiated by ModelAsController; this will create
* a controller based on the URLSegment action variable, by looking in the SiteTree table.
*
* @template T of SiteTree
* @todo Can this be used for anything other than SiteTree controllers?
*/
class ContentController extends Controller
{
/**
* @var T
* @var SiteTree
*/
protected $dataRecord;
@ -72,7 +72,7 @@ class ContentController extends Controller
* The ContentController will take the URLSegment parameter from the URL and use that to look
* up a SiteTree record.
*
* @param T|null $dataRecord
* @param SiteTree $dataRecord
*/
public function __construct($dataRecord = null)
{
@ -111,7 +111,7 @@ class ContentController extends Controller
* Return the children of a given page. The parent reference can either be a page link or an ID.
*
* @param string|int $parentRef
* @return SS_List<SiteTree>
* @return SS_List
*/
public function ChildrenOf($parentRef)
{
@ -175,6 +175,7 @@ class ContentController extends Controller
}
// Check page permissions
/** @skipUpgrade */
if ($this->dataRecord && $this->URLSegment != 'Security' && !$this->dataRecord->canView()) {
Security::permissionFailure($this);
return;
@ -185,10 +186,13 @@ class ContentController extends Controller
* This acts the same as {@link Controller::handleRequest()}, but if an action cannot be found this will attempt to
* fall over to a child controller in order to provide functionality for nested URLs.
*
* @param HTTPRequest $request
* @return HTTPResponse
* @throws HTTPResponse_Exception
*/
public function handleRequest(HTTPRequest $request): HTTPResponse
public function handleRequest(HTTPRequest $request)
{
/** @var SiteTree $child */
$child = null;
$action = $request->param('Action');
@ -196,6 +200,11 @@ class ContentController extends Controller
// control to a child controller. This allows for the creation of chains of controllers which correspond to a
// nested URL.
if ($action && SiteTree::config()->nested_urls && !$this->hasAction($action)) {
// See ModelAdController->getNestedController() for similar logic
if (class_exists('Translatable')) {
Translatable::disable_locale_filter();
}
$filter = URLSegmentFilter::create();
// look for a page with this URLSegment
@ -204,6 +213,10 @@ class ContentController extends Controller
// url encode unless it's multibyte (already pre-encoded in the database)
'URLSegment' => $filter->getAllowMultibyte() ? $action : rawurlencode($action),
])->first();
if (class_exists('Translatable')) {
Translatable::enable_locale_filter();
}
}
// we found a page with this URLSegment.
@ -213,6 +226,25 @@ class ContentController extends Controller
$response = ModelAsController::controller_for($child)->handleRequest($request);
} else {
// If a specific locale is requested, and it doesn't match the page found by URLSegment,
// look for a translation and redirect (see #5001). Only happens on the last child in
// a potentially nested URL chain.
if (class_exists('Translatable')) {
$locale = $request->getVar('locale');
if ($locale
&& i18n::getData()->validate($locale)
&& $this->dataRecord
&& $this->dataRecord->Locale != $locale
) {
$translation = $this->dataRecord->getTranslation($locale);
if ($translation) {
$response = new HTTPResponse();
$response->redirect($translation->Link(), 301);
throw new HTTPResponse_Exception($response);
}
}
}
Director::set_current_page($this->data());
try {
@ -243,7 +275,6 @@ class ContentController extends Controller
/**
* Returns the associated database record
* @return T
*/
public function data()
{
@ -255,7 +286,7 @@ class ContentController extends Controller
/**
* Returns a fixed navigation menu of the given level.
* @param int $level Menu level to return.
* @return ArrayList<SiteTree>
* @return ArrayList
*/
public function getMenu($level = 1)
{
@ -285,6 +316,7 @@ class ContentController extends Controller
// We might need to create a show in menu permission
if (isset($result)) {
foreach ($result as $page) {
/** @var SiteTree $page */
if ($page->canView()) {
$visible[] = $page;
}
@ -294,9 +326,6 @@ class ContentController extends Controller
return new ArrayList($visible);
}
/**
* @return ArrayList<SiteTree>
*/
public function Menu($level)
{
return $this->getMenu($level);
@ -305,6 +334,8 @@ class ContentController extends Controller
/**
* Returns the default log-in form.
*
* @todo Check if here should be returned just the default log-in form or
* all available log-in forms (also OpenID...)
* @return \SilverStripe\Security\MemberAuthenticator\MemberLoginForm
*/
public function LoginForm()
@ -384,6 +415,9 @@ HTML;
/**
* Returns an RFC1766 compliant locale string, e.g. 'fr-CA'.
* Inspects the associated {@link dataRecord} for a {@link SiteTree->Locale} value if present,
* and falls back to {@link Translatable::get_current_locale()} or {@link i18n::default_locale()},
* depending if Translatable is enabled.
*
* Suitable for insertion into lang= and xml:lang=
* attributes in HTML or XHTML output.
@ -392,7 +426,14 @@ HTML;
*/
public function ContentLocale()
{
$locale = i18n::get_locale();
if ($this->dataRecord && $this->dataRecord->hasExtension('Translatable')) {
$locale = $this->dataRecord->Locale;
} elseif (class_exists('Translatable') && SiteTree::has_extension('Translatable')) {
$locale = Translatable::get_current_locale();
} else {
$locale = i18n::get_locale();
}
return i18n::convert_rfc1766($locale);
}
@ -449,6 +490,7 @@ HTML;
$this->httpError(410);
}
// TODO Allow this to work when allow_url_fopen=0
if (isset($_SESSION['StatsID']) && $_SESSION['StatsID']) {
$url = 'http://ss2stat.silverstripe.com/Installation/installed?ID=' . $_SESSION['StatsID'];
@file_get_contents($url ?? '');

View File

@ -2,13 +2,9 @@
namespace SilverStripe\CMS\Controllers;
use SilverStripe\Admin\LeftAndMain;
use SilverStripe\CMS\Controllers\CMSMain;
use SilverStripe\Core\Extension;
/**
* @extends Extension<LeftAndMain>
*/
class LeftAndMainBatchActionsExtension extends Extension
{
public function updateBatchActionsForm(&$form)

View File

@ -5,7 +5,6 @@ namespace SilverStripe\CMS\Controllers;
use Psr\SimpleCache\CacheInterface;
use Psr\SimpleCache\InvalidArgumentException;
use ReflectionException;
use SilverStripe\Admin\LeftAndMain;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config;
@ -17,11 +16,10 @@ use SilverStripe\View\Requirements;
/**
* Extension to include custom page icons
*
* @extends Extension<LeftAndMain>
*/
class LeftAndMainPageIconsExtension extends Extension implements Flushable
{
/**
* @throws InvalidArgumentException
* @throws ReflectionException

View File

@ -17,6 +17,7 @@ use SilverStripe\Dev\Debug;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\View\Parsers\URLSegmentFilter;
use Translatable;
/**
* ModelAsController deals with mapping the initial request to the first {@link SiteTree}/{@link ContentController}
@ -32,9 +33,11 @@ class ModelAsController extends Controller implements NestedController
* Get the appropriate {@link ContentController} for handling a {@link SiteTree} object, link it to the object and
* return it.
*
* @param SiteTree $sitetree
* @param string $action
* @return ContentController
*/
public static function controller_for(SiteTree $sitetree, $action = null): ContentController
public static function controller_for(SiteTree $sitetree, $action = null)
{
$controller = $sitetree->getControllerName();
@ -55,6 +58,7 @@ class ModelAsController extends Controller implements NestedController
{
parent::beforeHandleRequest($request);
// If the database has not yet been created, redirect to the build page.
/** @skipUpgrade */
if (!DB::is_active() || !ClassInfo::hasTable('SiteTree')) {
$this->getResponse()->redirect(Controller::join_links(
Director::absoluteBaseURL(),
@ -68,8 +72,10 @@ class ModelAsController extends Controller implements NestedController
/**
* @uses ModelAsController::getNestedController()
* @param HTTPRequest $request
* @return HTTPResponse
*/
public function handleRequest(HTTPRequest $request): HTTPResponse
public function handleRequest(HTTPRequest $request)
{
$this->beforeHandleRequest($request);
@ -80,16 +86,23 @@ class ModelAsController extends Controller implements NestedController
}
// If the database has not yet been created, redirect to the build page.
/** @skipUpgrade */
if (!DB::is_active() || !ClassInfo::hasTable('SiteTree')) {
$this->getResponse()->redirect(Controller::join_links(Director::absoluteBaseURL(), 'dev/build?returnURL=' . (isset($_GET['url']) ? urlencode($_GET['url']) : null)));
$this->getResponse()->redirect(Director::absoluteBaseURL() . 'dev/build?returnURL=' . (isset($_GET['url']) ? urlencode($_GET['url']) : null));
$this->popCurrent();
return $this->getResponse();
}
try {
$result = $this->getNestedController()->handleRequest($this->getRequest());
$result = $result;
$result = $this->getNestedController();
if ($result instanceof RequestHandler) {
$result = $result->handleRequest($this->getRequest());
} elseif (!($result instanceof HTTPResponse)) {
user_error("ModelAsController::getNestedController() returned bad object type '" .
get_class($result)."'", E_USER_WARNING);
}
} catch (HTTPResponse_Exception $responseException) {
$result = $responseException->getResponse();
}
@ -99,9 +112,10 @@ class ModelAsController extends Controller implements NestedController
}
/**
* @return ContentController
* @throws Exception If URLSegment not passed in as a request parameter.
*/
public function getNestedController(): ContentController
public function getNestedController()
{
$request = $this->getRequest();
$urlSegment = $request->param('URLSegment');
@ -110,6 +124,11 @@ class ModelAsController extends Controller implements NestedController
throw new Exception('ModelAsController->getNestedController(): was not passed a URLSegment value.');
}
// Find page by link, regardless of current locale settings
if (class_exists('Translatable')) {
Translatable::disable_locale_filter();
}
// url encode unless it's multibyte (already pre-encoded in the database)
$filter = URLSegmentFilter::create();
@ -123,12 +142,24 @@ class ModelAsController extends Controller implements NestedController
if (SiteTree::config()->get('nested_urls')) {
$conditions[] = [sprintf('"%s"."ParentID"', $tableName) => 0];
}
/** @var SiteTree $sitetree */
$sitetree = DataObject::get_one(SiteTree::class, $conditions);
// Check translation module
// @todo Refactor out module specific code
if (class_exists('Translatable')) {
Translatable::enable_locale_filter();
}
if (!$sitetree) {
$this->httpError(404, 'The requested page could not be found.');
}
// Enforce current locale setting to the loaded SiteTree object
if (class_exists('Translatable') && $sitetree->Locale) {
Translatable::set_current_locale($sitetree->Locale);
}
if (isset($_REQUEST['debug'])) {
Debug::message("Using record #$sitetree->ID of type " . get_class($sitetree) . " with link {$sitetree->Link()}");
}

View File

@ -10,18 +10,17 @@ use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Core\Extension;
/**
* @extends Extension<ContentController|ModelAsController>
*/
class OldPageRedirector extends Extension
{
/**
* On every URL that generates a 404, we'll capture it here and see if we can
* find an old URL that it should be redirecting to.
*
* @param HTTPRequest $request The request object
* @throws HTTPResponse_Exception
*/
public function onBeforeHTTPError404(HTTPRequest $request)
public function onBeforeHTTPError404($request)
{
// We need to get the URL ourselves because $request->allParams() only has a max of 4 params
$params = preg_split('|/+|', $request->getURL() ?? '');
@ -77,6 +76,7 @@ class OldPageRedirector extends Extension
'ParentID' => is_numeric($parent) ? $parent : $parent->ID,
]);
}
/** @var SiteTree $page */
$page = $pages->first();
if (!$page) {
// If we haven't found a candidate, lets resort to finding an old page with this URL segment
@ -103,7 +103,7 @@ class OldPageRedirector extends Extension
// No valid page found.
if ($redirect) {
// If we had some redirect to be done, lets do it. imagine /foo/action -> /bar/action, we still want this redirect to happen if action isn't a page
return Controller::join_links($page->Link(), implode('/', $params));
return $page->Link() . implode('/', $params);
}
}
} else {

View File

@ -73,6 +73,7 @@ class RootURLController extends Controller implements Resettable
self::$is_at_root = true;
/** @skipUpgrade */
if (!DB::is_active() || !ClassInfo::hasTable('SiteTree')) {
$this->getResponse()->redirect(Controller::join_links(
Director::absoluteBaseURL(),
@ -84,12 +85,17 @@ class RootURLController extends Controller implements Resettable
}
}
public function handleRequest(HTTPRequest $request): HTTPResponse
/**
* @param HTTPRequest $request
* @return HTTPResponse
*/
public function handleRequest(HTTPRequest $request)
{
self::$is_at_root = true;
$this->beforeHandleRequest($request);
if (!$this->getResponse()->isFinished()) {
/** @skipUpgrade */
if (!DB::is_active() || !ClassInfo::hasTable('SiteTree')) {
$this->getResponse()->redirect(Director::absoluteBaseURL() . 'dev/build?returnURL=' . (isset($_GET['url']) ? urlencode($_GET['url']) : null));
return $this->getResponse();

View File

@ -0,0 +1,118 @@
<?php
namespace SilverStripe\CMS\Controllers;
use SilverStripe\ORM\CMSPreviewable;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\SS_List;
use SilverStripe\View\ViewableData;
/**
* Utility class representing links to different views of a record
* for CMS authors, usually for {@link SiteTree} objects with "stage" and "live" links.
* Useful both in the CMS and alongside the page template (for logged in authors).
* The class can be used for any {@link DataObject} subclass implementing the {@link CMSPreviewable} interface.
*
* New item types can be defined by extending the {@link SilverStripeNavigatorItem} class,
* for example the "cmsworkflow" module defines a new "future state" item with a date selector
* to view embargoed data at a future point in time. So the item doesn't always have to be a simple link.
*
* Class will be moved from `silverstripe/cms` to `silverstripe/admin`
* @deprecated 4.13.0 Will be renamed SilverStripe\Admin\Navigator\SilverStripeNavigator
*/
class SilverStripeNavigator extends ViewableData
{
/**
* @var DataObject|\SilverStripe\ORM\CMSPreviewable
*/
protected $record;
/**
* @param DataObject|\SilverStripe\ORM\CMSPreviewable $record
*/
public function __construct(CMSPreviewable $record)
{
Deprecation::withNoReplacement(function () {
Deprecation::notice(
'4.13.0',
'Will be renamed SilverStripe\Admin\Navigator\SilverStripeNavigator',
Deprecation::SCOPE_CLASS
);
});
parent::__construct();
$this->record = $record;
}
/**
* @return SS_List of SilverStripeNavigatorItem
*/
public function getItems()
{
$items = [];
$classes = ClassInfo::subclassesFor(SilverStripeNavigatorItem::class);
array_shift($classes);
// Sort menu items according to priority
foreach ($classes as $class) {
/** @var SilverStripeNavigatorItem $item */
$item = new $class($this->record);
if (!$item->canView()) {
continue;
}
// This funny litle formula ensures that the first item added with the same priority will be left-most.
$priority = $item->getPriority() * 100 - 1;
// Ensure that we can have duplicates with the same (default) priority
while (isset($items[$priority])) {
$priority++;
}
$items[$priority] = $item;
}
ksort($items);
// Drop the keys and let the ArrayList handle the numbering, so $IsFirst, $IsLast and others work properly.
return new ArrayList(array_values($items ?? []));
}
/**
* @return DataObject|\SilverStripe\ORM\CMSPreviewable
*/
public function getRecord()
{
return $this->record;
}
/**
* @param DataObject|CMSPreviewable $record
* @return array template data
*/
public static function get_for_record($record)
{
$html = '';
$message = '';
$navigator = new SilverStripeNavigator($record);
$items = $navigator->getItems();
foreach ($items as $item) {
$text = $item->getHTML();
if ($text) {
$html .= $text;
}
$newMessage = $item->getMessage();
if ($newMessage && $item->isActive()) {
$message = $newMessage;
}
}
return [
'items' => $html,
'message' => $message
];
}
}

View File

@ -0,0 +1,155 @@
<?php
namespace SilverStripe\CMS\Controllers;
use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\CMSPreviewable;
use SilverStripe\ORM\DataObject;
use SilverStripe\Versioned\Versioned;
use SilverStripe\Security\Member;
use SilverStripe\View\ViewableData;
/**
* Navigator items are links that appear in the $SilverStripeNavigator bar.
* To add an item, extend this class - it will be automatically picked up.
* When instanciating items manually, please ensure to call {@link canView()}.
*
* Class have been moved from `silverstripe/cms` to `silverstripe/admin` and renamed.
* @deprecated Will be renamed SilverStripe\Admin\Navigator\SilverStripeNavigatorItem
*/
abstract class SilverStripeNavigatorItem extends ViewableData
{
/**
* @param DataObject|CMSPreviewable
*/
protected $record;
/** @var string */
protected $recordLink;
public function __construct(CMSPreviewable $record)
{
Deprecation::withNoReplacement(function () {
switch (static::class) {
// These classes have their own deprecation notice
case 'SilverStripe\CMS\Controllers\SilverStripeNavigatorItem_ArchiveLink':
case 'SilverStripe\CMS\Controllers\SilverStripeNavigatorItem_LiveLink':
case 'SilverStripe\CMS\Controllers\SilverStripeNavigatorItem_StageLink':
case 'SilverStripe\CMS\Controllers\SilverStripeNavigatorItem_Unversioned':
// This class is not deprecated and doesn't have a deprecation notice
case 'SilverStripe\CMS\Controllers\SilverStripeNavigatorItem_CMSLink':
break;
default:
Deprecation::notice(
'4.13.0',
'Will be renamed SilverStripe\Admin\Navigator\SilverStripeNavigatorItem',
Deprecation::SCOPE_CLASS
);
}
});
parent::__construct();
$this->record = $record;
}
/**
* @return string HTML, mostly a link - but can be more complex as well.
* For example, a "future state" item might show a date selector.
*/
abstract public function getHTML();
/**
* @return string
* Get the Title of an item
*/
abstract public function getTitle();
/**
* Machine-friendly name.
*
* @return string
*/
public function getName()
{
return substr(static::class, strpos(static::class, '_') + 1);
}
/**
* Optional link to a specific view of this record.
* Not all items are simple links, please use {@link getHTML()}
* to represent an item in markup unless you know what you're doing.
*
* @return string
*/
public function getLink()
{
return null;
}
/**
* @return string
*/
public function getMessage()
{
return null;
}
/**
* @return DataObject
*/
public function getRecord()
{
return $this->record;
}
/**
* @return int
*/
public function getPriority()
{
return $this->config()->get('priority');
}
/**
* As items might convey different record states like a "stage" or "live" table,
* an item can be active (showing the record in this state).
*
* @return boolean
*/
public function isActive()
{
return false;
}
/**
* Filters items based on member permissions or other criteria,
* such as if a state is generally available for the current record.
*
* @param Member $member
* @return Boolean
*/
public function canView($member = null)
{
return true;
}
/**
* Counts as "archived" if the current record is a different version from both live and draft.
*
* @return boolean
*/
public function isArchived()
{
/** @var Versioned|DataObject $record */
$record = $this->record;
if (!$record->hasExtension(Versioned::class) || !$record->hasStages()) {
return false;
}
if (!isset($record->_cached_isArchived)) {
$record->_cached_isArchived = $record->isArchived();
}
return $record->_cached_isArchived;
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace SilverStripe\CMS\Controllers;
use SilverStripe\CMS\Model\RedirectorPage;
use SilverStripe\Control\Controller;
use SilverStripe\Core\Convert;
use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\CMSPreviewable;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\Versioned\Versioned;
/**
* Class will be moved from `silverstripe/cms` to `silverstripe/admin`
* @deprecated 4.13.0 Will be renamed SilverStripe\VersionedAdmin\Navigator\SilverStripeNavigatorItem_ArchiveLink
*/
class SilverStripeNavigatorItem_ArchiveLink extends SilverStripeNavigatorItem
{
/**
* @param DataObject|CMSPreviewable $record
*/
public function __construct(CMSPreviewable $record)
{
Deprecation::withNoReplacement(function () {
Deprecation::notice(
'4.13.0',
'Will be renamed SilverStripe\VersionedAdmin\Navigator\SilverStripeNavigatorItem_ArchiveLink',
Deprecation::SCOPE_CLASS
);
});
parent::__construct($record);
}
/** @config */
private static $priority = 40;
public function getHTML()
{
$linkClass = $this->isActive() ? 'ss-ui-button current' : 'ss-ui-button';
$linkTitle = _t('SilverStripe\\CMS\\Controllers\\ContentController.ARCHIVEDSITE', 'Preview version');
$recordLink = Convert::raw2att(Controller::join_links(
$this->record->AbsoluteLink(),
'?archiveDate=' . urlencode($this->record->LastEdited ?? '')
));
return "<a class=\"{$linkClass}\" href=\"$recordLink\" target=\"_blank\">$linkTitle</a>";
}
public function getTitle()
{
return _t('SilverStripe\\CMS\\Controllers\\SilverStripeNavigator.ARCHIVED', 'Archived');
}
public function getMessage()
{
$date = Versioned::current_archived_date();
if (empty($date)) {
return null;
}
/** @var DBDatetime $dateObj */
$dateObj = DBField::create_field('Datetime', $date);
$title = _t('SilverStripe\\CMS\\Controllers\\ContentController.NOTEWONTBESHOWN', 'Note: this message will not be shown to your visitors');
return "<div id=\"SilverStripeNavigatorMessage\" title=\"{$title}\">"
. _t('SilverStripe\\CMS\\Controllers\\ContentController.ARCHIVEDSITEFROM', 'Archived site from')
. "<br />" . $dateObj->Nice() . "</div>";
}
public function getLink()
{
$link = $this->record->PreviewLink();
return $link ? Controller::join_links($link, '?archiveDate=' . urlencode($this->record->LastEdited ?? '')) : '';
}
public function canView($member = null)
{
/** @var Versioned|DataObject $record */
$record = $this->record;
return (
$record->hasExtension(Versioned::class)
&& $record->hasStages()
&& $this->isArchived()
// Don't follow redirects in preview, they break the CMS editing form
&& !($record instanceof RedirectorPage)
&& $this->getLink()
);
}
public function isActive()
{
return $this->isArchived();
}
}

View File

@ -3,7 +3,6 @@
namespace SilverStripe\CMS\Controllers;
use SilverStripe\Admin\LeftAndMain;
use SilverStripe\Admin\Navigator\SilverStripeNavigatorItem;
use SilverStripe\CMS\Model\RedirectorPage;
use SilverStripe\Control\Controller;

View File

@ -0,0 +1,106 @@
<?php
namespace SilverStripe\CMS\Controllers;
use SilverStripe\Control\Controller;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\CMSPreviewable;
use SilverStripe\ORM\DataObject;
use SilverStripe\Versioned\Versioned;
/**
* Class will be moved from `silverstripe/cms` to `silverstripe/admin`.
* @deprecated 4.13.0 Will be renamed SilverStripe\VersionedAdmin\Navigator\SilverStripeNavigatorItem_LiveLink
*/
class SilverStripeNavigatorItem_LiveLink extends SilverStripeNavigatorItem
{
public function __construct(CMSPreviewable $record)
{
Deprecation::withNoReplacement(function () {
Deprecation::notice(
'4.13.0',
'Will be renamed SilverStripe\VersionedAdmin\Navigator\SilverStripeNavigatorItem_LiveLink',
Deprecation::SCOPE_CLASS
);
});
parent::__construct($record);
}
/** @config */
private static $priority = 30;
public function getHTML()
{
$livePage = $this->getLivePage();
if (!$livePage) {
return null;
}
$linkClass = $this->isActive() ? 'class="current" ' : '';
$linkTitle = _t('SilverStripe\\CMS\\Controllers\\ContentController.PUBLISHEDSITE', 'Published Site');
$recordLink = Convert::raw2att(Controller::join_links($livePage->AbsoluteLink(), "?stage=Live"));
return "<a {$linkClass} href=\"$recordLink\">$linkTitle</a>";
}
public function getTitle()
{
return _t(
'SilverStripe\\CMS\\Controllers\\ContentController.PUBLISHED',
'Published',
'Used for the Switch between draft and published view mode. Needs to be a short label'
);
}
public function getMessage()
{
return "<div id=\"SilverStripeNavigatorMessage\" title=\"" . _t(
'SilverStripe\\CMS\\Controllers\\ContentController.NOTEWONTBESHOWN',
'Note: this message will not be shown to your visitors'
) . "\">" . _t(
'SilverStripe\\CMS\\Controllers\\ContentController.PUBLISHEDSITE',
'Published Site'
) . "</div>";
}
public function getLink()
{
$link = $this->getLivePage()->PreviewLink();
return $link ? Controller::join_links($link, '?stage=Live') : '';
}
public function canView($member = null)
{
/** @var Versioned|DataObject $record */
$record = $this->record;
return (
$record->hasExtension(Versioned::class)
&& $this->showLiveLink()
&& $record->hasStages()
&& $this->getLivePage()
&& $this->getLink()
);
}
/**
* @return bool
*/
public function showLiveLink()
{
return (bool)Config::inst()->get(get_class($this->record), 'show_live_link');
}
public function isActive()
{
return (
(!Versioned::get_stage() || Versioned::get_stage() == 'Live')
&& !$this->isArchived()
);
}
protected function getLivePage()
{
$baseClass = $this->record->baseClass();
return Versioned::get_by_stage($baseClass, Versioned::LIVE)->byID($this->record->ID);
}
}

View File

@ -0,0 +1,115 @@
<?php
namespace SilverStripe\CMS\Controllers;
use SilverStripe\Control\Controller;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Convert;
use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\CMSPreviewable;
use SilverStripe\ORM\DataObject;
use SilverStripe\Versioned\Versioned;
use SiteTreeFutureState;
/**
* Class will be moved from `silverstripe/cms` to `silverstripe/admin`.
* @deprecated 4.13.0 Will be renamed SilverStripe\VersionedAdmin\Navigator\SilverStripeNavigatorItem_StageLink
*/
class SilverStripeNavigatorItem_StageLink extends SilverStripeNavigatorItem
{
public function __construct(CMSPreviewable $record)
{
Deprecation::withNoReplacement(function () {
Deprecation::notice(
'4.13.0',
'Will be renamed SilverStripe\VersionedAdmin\Navigator\SilverStripeNavigatorItem_StageLink',
Deprecation::SCOPE_CLASS
);
});
parent::__construct($record);
}
/** @config */
private static $priority = 20;
public function getHTML()
{
$draftPage = $this->getDraftPage();
if (!$draftPage) {
return null;
}
$linkClass = $this->isActive() ? 'class="current" ' : '';
$linkTitle = _t('SilverStripe\\CMS\\Controllers\\ContentController.DRAFTSITE', 'Draft Site');
$recordLink = Convert::raw2att(Controller::join_links($draftPage->AbsoluteLink(), "?stage=Stage"));
return "<a {$linkClass} href=\"$recordLink\">$linkTitle</a>";
}
public function getTitle()
{
return _t(
'SilverStripe\\CMS\\Controllers\\ContentController.DRAFT',
'Draft',
'Used for the Switch between draft and published view mode. Needs to be a short label'
);
}
public function getMessage()
{
return "<div id=\"SilverStripeNavigatorMessage\" title=\"" . _t(
'SilverStripe\\CMS\\Controllers\\ContentController.NOTEWONTBESHOWN',
'Note: this message will not be shown to your visitors'
) . "\">" . _t(
'SilverStripe\\CMS\\Controllers\\ContentController.DRAFTSITE',
'Draft Site'
) . "</div>";
}
public function getLink()
{
$date = Versioned::current_archived_date();
$link = $this->record->PreviewLink();
if (!$link) {
return '';
}
return Controller::join_links(
$link,
'?stage=Stage',
$date ? '?archiveDate=' . $date : null
);
}
public function canView($member = null)
{
/** @var Versioned|DataObject $record */
$record = $this->record;
return (
$record->hasExtension(Versioned::class)
&& $this->showStageLink()
&& $record->hasStages()
&& $this->getDraftPage()
&& $this->getLink()
);
}
/**
* @return bool
*/
public function showStageLink()
{
return (bool)Config::inst()->get(get_class($this->record), 'show_stage_link');
}
public function isActive()
{
return (
Versioned::get_stage() == 'Stage'
&& !(ClassInfo::exists('SiteTreeFutureState') && SiteTreeFutureState::get_future_datetime())
&& !$this->isArchived()
);
}
protected function getDraftPage()
{
$baseClass = $this->record->baseClass();
return Versioned::get_by_stage($baseClass, Versioned::DRAFT)->byID($this->record->ID);
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace SilverStripe\CMS\Controllers;
use SilverStripe\CMS\Controllers\SilverStripeNavigatorItem;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\CMSPreviewable;
use SilverStripe\Security\Member;
use SilverStripe\Versioned\Versioned;
/**
* Class will be moved from `silverstripe/cms` to `silverstripe/admin`.
* @deprecated 4.13.0 Will be renamed SilverStripe\Admin\Navigator\SilverStripeNavigatorItem_Unversioned
*/
class SilverStripeNavigatorItem_Unversioned extends SilverStripeNavigatorItem
{
public function __construct(CMSPreviewable $record)
{
Deprecation::withNoReplacement(function () {
Deprecation::notice(
'4.13.0',
'Will be renamed SilverStripe\Admin\Navigator\SilverStripeNavigatorItem_Unversioned',
Deprecation::SCOPE_CLASS
);
});
parent::__construct($record);
}
public function getHTML()
{
$recordLink = Convert::raw2att($this->getLink());
$linkTitle = _t('SilverStripe\\CMS\\Controllers\\ContentController.UNVERSIONEDPREVIEW', 'Preview');
return "<a class=\"current\" href=\"$recordLink\">$linkTitle</a>";
}
public function getLink()
{
return $this->getRecord()->PreviewLink() ?? '';
}
public function getTitle()
{
return _t(
'SilverStripe\\CMS\\Controllers\\ContentController.UNVERSIONEDPREVIEW',
'Preview',
'Used for the Switch between states (if any other other states are added). Needs to be a short label'
);
}
/**
* True if the record doesn't have the Versioned extension and is configured to display this item.
*
* @param Member $member
* @return bool
*/
public function canView($member = null)
{
return (
$this->recordIsUnversioned()
&& $this->showUnversionedLink()
&& $this->getLink()
);
}
private function recordIsUnversioned(): bool
{
$record = $this->getRecord();
// If the record has the Versioned extension, it can be considered unversioned
// for the purposes of this class if it has no stages and is not archived.
if ($record->hasExtension(Versioned::class)) {
return (!$record->hasStages()) && !$this->isArchived();
}
// Completely unversioned.
return true;
}
/**
* True if the record is configured to display this item.
*
* @return bool
*/
public function showUnversionedLink(): bool
{
return (bool) Config::inst()->get(get_class($this->record), 'show_unversioned_preview_link');
}
/**
* This item is always active, as there are unlikely to be other preview states available for the record.
*
* @return bool
*/
public function isActive()
{
return true;
}
}

View File

@ -9,8 +9,7 @@ use SilverStripe\Forms\Form;
/**
* Decorates ModalController with insert internal link
*
* @extends Extension<ModalController>
* @see ModalController
*/
class InternalLinkModalExtension extends Extension
{
@ -23,6 +22,17 @@ class InternalLinkModalExtension extends Extension
'editorAnchorLink',
];
/**
* @return ModalController
*/
public function getOwner()
{
/** @var ModalController $owner */
$owner = $this->owner;
return $owner;
}
/**
* Form for inserting internal link pages
*

View File

@ -134,7 +134,7 @@ class SiteTreeURLSegmentField extends TextField
*/
public function getURLPrefix()
{
return rtrim($this->urlPrefix ?? '', '/') . '/';
return $this->urlPrefix;
}
public function getURLSuffix()

View File

@ -18,8 +18,8 @@ use SilverStripe\Versioned\Versioned;
* @property string $RedirectionType Either 'Internal','External' or 'File'
* @property string $ExternalURL URL to redirect to if $RedirectionType is 'External'
* @property int $LinkToID
* @method SiteTree LinkTo()
* @method File LinkToFile()
* @method SiteTree LinkTo() Page to link to if $RedirectionType is 'Internal'
* @method File LinkToFile() File to link to if $RedirectionType is 'File'
*/
class RedirectorPage extends Page
{
@ -120,6 +120,7 @@ class RedirectorPage extends Page
}
// Check internal redirect
/** @var SiteTree $linkTo */
$linkTo = $this->LinkToID ? SiteTree::get()->byID($this->LinkToID) : null;
if (empty($linkTo)) {
@ -163,6 +164,7 @@ class RedirectorPage extends Page
$this->HasBrokenLink = true;
}
} else {
// TODO implement checking of a remote site
$this->HasBrokenLink = false;
}
}

View File

@ -3,48 +3,27 @@ namespace SilverStripe\CMS\Model;
use SilverStripe\Control\HTTPRequest;
use PageController;
use SilverStripe\Control\HTTPResponse_Exception;
/**
* Controller for the {@link RedirectorPage}.
*
* @extends PageController<RedirectorPage>
*/
class RedirectorPageController extends PageController
{
private static $allowed_actions = ['index'];
/**
* Should respond with HTTP 404 if the page or file being redirected to is missing
*/
private static bool $missing_redirect_is_404 = true;
/**
* Check we don't already have a redirect code set
*
* @param HTTPRequest $request
* @return \SilverStripe\Control\HTTPResponse
* @throws HTTPResponse_Exception
*/
public function index(HTTPRequest $request)
{
/** @var RedirectorPage $page */
$page = $this->data();
// Redirect if we can
if (!$this->getResponse()->isFinished() && $link = $page->redirectionLink()) {
return $this->redirect($link, 301);
$this->redirect($link, 301);
}
// Respond with 404 if redirecting to a missing file or page
if (($this->RedirectionType === 'Internal' && !$page->LinkTo()?->exists())
|| ($this->RedirectionType === 'File' && !$page->LinkToFile()?->exists())
) {
if (static::config()->get('missing_redirect_is_404')) {
$this->httpError(404);
}
}
// Fall back to a message about the bad setup
return parent::handleAction($request, 'handleIndex');
}

View File

@ -25,6 +25,7 @@ use SilverStripe\Core\Manifest\ModuleResource;
use SilverStripe\Core\Manifest\ModuleResourceLoader;
use SilverStripe\Core\Manifest\VersionProvider;
use SilverStripe\Core\Resettable;
use SilverStripe\Dev\Deprecation;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\CompositeField;
use SilverStripe\Forms\DropdownField;
@ -35,10 +36,8 @@ use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldDataColumns;
use SilverStripe\Forms\GridField\GridFieldLazyLoader;
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
use SilverStripe\Forms\ListboxField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\OptionsetField;
use SilverStripe\Forms\SearchableMultiDropdownField;
use SilverStripe\Forms\Tab;
use SilverStripe\Forms\TabSet;
use SilverStripe\Forms\TextareaField;
@ -103,13 +102,17 @@ use SilverStripe\View\SSViewer;
* @property bool $HasBrokenFile True if this page has a broken file shortcode
* @property bool $HasBrokenLink True if this page has a broken page shortcode
*
* @method ManyManyList ViewerGroups() List of groups that can view this object.
* @method ManyManyList EditorGroups() List of groups that can edit this object.
* @method SiteTree Parent()
* @method HasManyList|SiteTreeLink[] BackLinks() List of SiteTreeLink objects attached to this page
*
* @mixin Hierarchy
* @mixin Versioned
* @mixin RecursivePublishable
* @mixin SiteTreeLinkTracking Added via linktracking.yml to DataObject directly
* @mixin FileLinkTracking Added via filetracking.yml in silverstripe/assets
* @mixin InheritedPermissionsExtension
* @method HasManyList<SiteTreeLink> BackLinks()
*/
class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvider, CMSPreviewable, Resettable, Flushable, MemberCacheFlusher
{
@ -200,20 +203,11 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
* in the cms, set this to the old class name. Eg, if you extended Product
* to make ImprovedProduct, then you would set $hide_ancestor to Product.
*
* @deprecated 5.2.0 Use hide_pagetypes instead
*
* @config
* @var string
*/
private static $hide_ancestor = null;
/**
* Any fully qualified class names added to this array will be hidden in the CMS
* when selecting page types, e.g. for creating a new page or changing the type
* of an existing page.
*/
private static array $hide_pagetypes = [];
/**
* You can define the class of the controller that maps to your SiteTree object here if
* you don't want to rely on the magic of appending Controller to the Classname
@ -544,7 +538,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
}
/**
* Return a subclass map of SiteTree that shouldn't be hidden through {@link SiteTree::$hide_pagetypes}
* Return a subclass map of SiteTree that shouldn't be hidden through {@link SiteTree::$hide_ancestor}
*
* @return array
*/
@ -557,7 +551,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
unset($classes[$baseClassIndex]);
}
$kill_ancestors = self::config()->get('hide_pagetypes', Config::UNINHERITED) ?? [];
$kill_ancestors = [];
// figure out if there are any classes we don't want to appear
foreach ($classes as $class) {
@ -607,6 +601,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
return null; // There were no suitable matches at all.
}
/** @var SiteTree $page */
$link = Convert::raw2att($page->Link());
if ($content) {
@ -644,7 +639,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
if ($this->hasMethod('alternateAbsoluteLink')) {
return $this->alternateAbsoluteLink($action);
} else {
return Director::absoluteURL((string) $this->Link($action));
return Director::absoluteURL($this->Link($action));
}
}
@ -657,6 +652,13 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
*/
public function PreviewLink($action = null)
{
if ($this->hasMethod('alternatePreviewLink')) {
Deprecation::withNoReplacement(function () use ($action) {
Deprecation::notice('5.0', 'Use updatePreviewLink or override PreviewLink method');
return $this->alternatePreviewLink($action);
});
}
$link = $this->AbsoluteLink($action);
$this->extend('updatePreviewLink', $link, $action);
return $link;
@ -697,17 +699,15 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
$base = $this->URLSegment;
}
$this->extend('updateRelativeLink', $base, $action);
// Legacy support: If $action === true, retain URLSegment for homepages,
// but don't append any action
if ($action === true) {
$action = null;
}
$link = Controller::join_links($base, $action);
$this->extend('updateRelativeLink', $link, $base, $action);
return $link;
return Controller::join_links($base, '/', $action);
}
/**
@ -884,10 +884,12 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
*/
public function duplicateWithChildren()
{
/** @var SiteTree $clone */
$clone = $this->duplicate();
$children = $this->AllChildren();
if ($children) {
/** @var SiteTree $child */
$sort = 0;
foreach ($children as $child) {
$childClone = method_exists($child, 'duplicateWithChildren')
@ -910,6 +912,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
*/
public function duplicateAsChild($id)
{
/** @var SiteTree $newSiteTree */
$newSiteTree = $this->duplicate();
$newSiteTree->ParentID = $id;
$newSiteTree->Sort = 0;
@ -945,7 +948,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
* @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
* @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
*
* @return ArrayList<SiteTree>
* @return ArrayList
*/
public function getBreadcrumbItems($maxDepth = 20, $stopAtPageType = false, $showHidden = false)
{
@ -1190,14 +1193,6 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
return true;
}
// check for specific users
if ($this->CanViewType === InheritedPermissions::ONLY_THESE_MEMBERS
&& $member
&& $this->ViewerMembers()->filter('ID', $member->ID)->count() > 0
) {
return true;
}
return false;
}
@ -1411,6 +1406,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
if (eval("return $condition;")) {
$collator[] = $item;
}
/** @var SiteTree $item */
$item->collateDescendants($condition, $collator);
}
return true;
@ -1762,6 +1758,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
// If deleting this page, delete all its children.
if ($this->isInDB() && SiteTree::config()->get('enforce_strict_hierarchy')) {
foreach ($this->AllChildren() as $child) {
/** @var SiteTree $child */
$child->delete();
}
}
@ -1959,10 +1956,11 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
/**
* Get the back-link tracking objects that link to this page
*
* @return ArrayList<DataObject>
* @return ArrayList|DataObject[]
*/
public function BackLinkTracking()
{
// @todo - Implement PolymorphicManyManyList to replace this
$list = ArrayList::create();
$siteTreelinkTable = SiteTreeLink::singleton()->baseTable();
@ -2000,7 +1998,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
* Returns the pages that depend on this page. This includes virtual pages, pages that link to it, etc.
*
* @param bool $includeVirtuals Set to false to exlcude virtual pages.
* @return ArrayList<SiteTree>
* @return ArrayList|SiteTree[]
*/
public function DependentPages($includeVirtuals = true)
{
@ -2050,7 +2048,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
/**
* Return all virtual pages that link to this page.
*
* @return DataList<SiteTree>
* @return DataList
*/
public function VirtualPages()
{
@ -2097,6 +2095,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
false,
$dependentPages
);
/** @var GridFieldDataColumns $dataColumns */
$dataColumns = $dependentTable->getConfig()->getComponentByType(GridFieldDataColumns::class);
$dataColumns
->setDisplayFields($dependentColumns)
@ -2281,13 +2280,6 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
_t(__CLASS__.'.VIEWERGROUPS', "Viewer Groups"),
Group::class
),
$viewerMembersField = SearchableMultiDropdownField::create(
"ViewerMembers",
_t(__CLASS__.'.VIEWERMEMBERS', "Viewer Users"),
Member::get(),
)
->setIsLazyLoaded(true)
->setUseSearchContext(true),
$editorsOptionsField = new OptionsetField(
"CanEditType",
_t(__CLASS__.'.EDITHEADER', "Who can edit this page?")
@ -2296,14 +2288,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
"EditorGroups",
_t(__CLASS__.'.EDITORGROUPS', "Editor Groups"),
Group::class
),
$editorMembersField = SearchableMultiDropdownField::create(
"EditorMembers",
_t(__CLASS__.'.EDITORMEMBERS', "Editor Users"),
Member::get()
)
->setIsLazyLoaded(true)
->setUseSearchContext(true)
)
)
);
@ -2343,10 +2328,6 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
__CLASS__.'.ACCESSONLYTHESE',
"Only these groups (choose from list)"
),
InheritedPermissions::ONLY_THESE_MEMBERS => _t(
__CLASS__.'.ACCESSONLYMEMBERS',
"Only these users (choose from list)"
),
];
$viewersOptionsField->setSource($viewersOptionsSource);
@ -2373,27 +2354,17 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
if (!Permission::check('SITETREE_GRANT_ACCESS')) {
$fields->makeFieldReadonly($viewersOptionsField);
if ($this->CanViewType === InheritedPermissions::ONLY_THESE_USERS) {
if ($this->CanEditType === InheritedPermissions::ONLY_THESE_USERS) {
$fields->makeFieldReadonly($viewerGroupsField);
$fields->removeByName('ViewerMembers');
} elseif ($this->CanViewType === InheritedPermissions::ONLY_THESE_MEMBERS) {
$fields->makeFieldReadonly($viewerMembersField);
$fields->removeByName('ViewerGroups');
} else {
$fields->removeByName('ViewerGroups');
$fields->removeByName('ViewerMembers');
}
$fields->makeFieldReadonly($editorsOptionsField);
if ($this->CanEditType === InheritedPermissions::ONLY_THESE_USERS) {
$fields->makeFieldReadonly($editorGroupsField);
$fields->removeByName('EditorMembers');
} elseif ($this->CanEditType === InheritedPermissions::ONLY_THESE_MEMBERS) {
$fields->makeFieldReadonly($editorMembersField);
$fields->removeByName('EditorGroups');
} else {
$fields->removeByName('EditorGroups');
$fields->removeByName('EditorMembers');
}
}
@ -2502,6 +2473,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
// "readonly"/viewing version that isn't the current version of the record
/** @var SiteTree $stageRecord */
$stageRecord = Versioned::get_by_stage(static::class, Versioned::DRAFT)->byID($this->ID);
/** @skipUpgrade */
if ($stageRecord && $stageRecord->Version != $this->Version) {
$moreOptions->push(FormAction::create('email', _t('SilverStripe\\CMS\\Controllers\\CMSMain.EMAIL', 'Email')));
$moreOptions->push(FormAction::create('rollback', _t('SilverStripe\\CMS\\Controllers\\CMSMain.ROLLBACK', 'Roll back to this version')));
@ -2546,7 +2518,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
// Note: It would be nice to have a canRestore() permission at some point
if ($canEdit && !$isOnDraft && !$isPublished) {
// Determine if we should force a restore to root (where once it was a subpage)
$restoreToRoot = $this->isParentArchived() && $this->config()->get('can_be_root');
$restoreToRoot = $this->isParentArchived();
// "restore"
$title = $restoreToRoot
@ -2555,15 +2527,13 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
$description = $restoreToRoot
? _t('SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE_TO_ROOT_DESC', 'Restore the archived version to draft as a top level page')
: _t('SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE_DESC', 'Restore the archived version to draft');
if (!$this->isParentArchived() || $restoreToRoot) {
$majorActions->push(
FormAction::create('restore', $title)
->setDescription($description)
->setAttribute('data-to-root', $restoreToRoot)
->addExtraClass('btn-warning font-icon-back-in-time')
->setUseButtonTag(true)
);
}
$majorActions->push(
FormAction::create('restore', $title)
->setDescription($description)
->setAttribute('data-to-root', $restoreToRoot)
->addExtraClass('btn-warning font-icon-back-in-time')
->setUseButtonTag(true)
);
}
// If a page is on any stage it can be archived
@ -2825,6 +2795,35 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
return $allowedChildren;
}
/**
* @deprecated 4.12.0 Use creatableChildPages() instead
*
* Gets a list of the page types that can be created under this specific page
*
* @return array
*/
public function creatableChildren()
{
Deprecation::notice('4.12.0', 'Use creatableChildPages() instead');
// Build the list of candidate children
$cache = SiteTree::singleton()->getCreatableChildrenCache();
$cacheKey = $this->generateChildrenCacheKey(Security::getCurrentUser() ? Security::getCurrentUser()->ID : 0);
$children = $cache->get($cacheKey, []);
if (!$children || !isset($children[$this->ID])) {
$children[$this->ID] = [];
$candidates = static::page_type_classes();
foreach ($candidates as $childClass) {
$child = singleton($childClass);
if ($child->canCreate(null, ['Parent' => $this])) {
$children[$this->ID][$childClass] = $child->i18n_singular_name();
}
}
$cache->set($cacheKey, $children);
}
return $children[$this->ID];
}
/**
*
* Gets a list of the page types that can be created under this specific page, including font icons
@ -3073,6 +3072,14 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
// If we have a class of "{$ClassName}Controller" then we found our controller
if (class_exists($candidate = sprintf('%sController', $class))) {
return $candidate;
} elseif (class_exists($candidate = sprintf('%s_Controller', $class))) {
// Support the legacy underscored filename, but raise a deprecation notice
Deprecation::notice(
'5.0',
'Underscored controller class names are deprecated. Use "MyController" instead of "My_Controller".',
Deprecation::SCOPE_GLOBAL
);
return $candidate;
} elseif (is_array($namespaceMap)) {
foreach ($namespaceMap as $pageNamespace => $controllerNamespace) {
if (strpos($class, $pageNamespace) !== 0) {
@ -3325,6 +3332,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
$this->flushCache();
// Need to mark pages depending to this one as broken
/** @var Page $page */
foreach ($this->DependentPages() as $page) {
// Update sync link tracking
$page->syncLinkTracking();
@ -3335,7 +3343,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
}
/**
* Cache key for creatableChildPages() method
* Cache key for creatableChildren() method
*
* @param int $memberID
* @return string

View File

@ -7,9 +7,6 @@ use SilverStripe\Security\Member;
/**
* Plug-ins for additional functionality in your SiteTree classes.
*
* @template T of SiteTree
* @extends DataExtension<T>
*/
abstract class SiteTreeExtension extends DataExtension
{
@ -78,15 +75,13 @@ abstract class SiteTreeExtension extends DataExtension
* before {@link SiteTree::RelativeLink()} calls {@link Controller::join_links()}
* on the $base and $action
*
* @param string &$link The URL of this page relative to siteroot including
* @param string &$base The URL of this page relative to siteroot, not including
* the action
* @param string $base The URL of this page relative to siteroot, not including
* the action
* @param string|boolean $action The action or subpage called on this page.
* @param string|boolean &$action The action or subpage called on this page.
* (Legacy support) If this is true, then do not reduce the 'home' urlsegment
* to an empty link
*/
public function updateRelativeLink(&$link, $base, $action)
public function updateRelativeLink(&$base, &$action)
{
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace SilverStripe\CMS\Model;
use SilverStripe\Dev\Deprecation;
use SilverStripe\Assets\File;
use SilverStripe\ORM\DataExtension;
use SilverStripe\View\SSViewer;
/**
* @deprecated 4.2.0 Use FileLinkTracking instead
* @property File $owner
*/
class SiteTreeFileExtension extends DataExtension
{
private static $casting = [
'BackLinkHTMLList' => 'HTMLFragment'
];
/**
* Generate an HTML list which provides links to where a file is used.
*
* @return string
*/
public function __construct()
{
Deprecation::notice('4.2.0', 'Use FileLinkTracking instead', Deprecation::SCOPE_CLASS);
}
public function BackLinkHTMLList()
{
$viewer = SSViewer::create(['type' => 'Includes', self::class . '_description']);
return $viewer->process($this->owner);
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace SilverStripe\CMS\Model;
use SilverStripe\Assets\File;
use SilverStripe\Dev\Deprecation;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Tab;
use SilverStripe\Forms\TabSet;
use SilverStripe\ORM\DataExtension;
use SilverStripe\Admin\Forms\UsedOnTable;
use SilverStripe\Versioned\RecursivePublishable;
/**
* @deprecated 4.12.0 Use UsedOnTable instead
*
* Extension applied to {@see FileFormFactory} to decorate with a "Used on:" information area.
* Uses tracking provided by {@see SiteTreeFileExtension} to generate this.
*
* @property File $owner
* @deprecated 4.12.0 Use UsedOnTable instead
*/
class SiteTreeFileFormFactoryExtension extends DataExtension
{
public function __construct()
{
Deprecation::notice('4.12.0', 'Use UsedOnTable instead', Deprecation::SCOPE_CLASS);
}
/**
* @deprecated 4.12.0 Use UsedOnTable instead
*/
public function updateFormFields(FieldList $fields, $controller, $formName, $context)
{
Deprecation::notice('4.12.0', 'Use UsedOnTable instead');
/** @var TabSet $tabset */
$tabset = $fields->fieldByName('Editor');
if (!$tabset) {
return;
}
$usedOnField = UsedOnTable::create('UsedOnTableReplacement');
$usedOnField->setRecord($context['Record']);
// Add field to new tab
/** @var Tab $tab */
$tab = Tab::create('Usage', _t(__CLASS__ . '.USAGE', 'Usage'), $usedOnField);
$tabset->push($tab);
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace SilverStripe\CMS\Model;
use SilverStripe\Assets\File;
use SilverStripe\Assets\Folder;
use SilverStripe\Assets\Shortcodes\FileLink;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataExtension;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject;
/**
* @deprecated 4.2.0 Will be removed without equivalent functionality to replace it
*/
class SiteTreeFolderExtension extends DataExtension
{
public function __construct()
{
Deprecation::notice('4.2.0', 'Will be removed without equivalent functionality to replace it', Deprecation::SCOPE_CLASS);
parent::__construct();
}
/**
* Looks for files used in system and create where clause which contains all ID's of files.
*
* @deprecated 4.2.0 Will be removed without equivalent functionality to replace it
* @returns string where clause which will work as filter.
*/
public function getUnusedFilesListFilter()
{
Deprecation::notice('4.2.0', 'Will be removed without equivalent functionality to replace it');
// Add all records in link tracking
$usedFiles = FileLink::get()->column('LinkedID');
// Get all classes that aren't folder
$fileClasses = array_diff_key(
ClassInfo::subclassesFor(File::class) ?? [],
ClassInfo::subclassesFor(Folder::class)
);
// Search on a class-by-class basis
$classes = ClassInfo::subclassesFor(SiteTree::class);
$schema = DataObject::getSchema();
foreach ($classes as $className) {
// Build query based on all direct has_ones on this class
$hasOnes = Config::inst()->get($className, 'has_one', Config::UNINHERITED);
if (empty($hasOnes)) {
continue;
}
$where = [];
$columns = [];
foreach ($hasOnes as $relName => $joinClass) {
if (in_array($joinClass, $fileClasses ?? [])) {
$column = $relName . 'ID';
$columns[] = $column;
$quotedColumn = $schema->sqlColumnForField($className, $column);
$where[] = "{$quotedColumn} > 0";
}
}
// Get all records with any file ID in the searched columns
$recordsArray = DataList::create($className)->whereAny($where)->toArray();
$records = ArrayList::create($recordsArray);
foreach ($columns as $column) {
$usedFiles = array_unique(array_merge($usedFiles, $records->column($column)));
}
}
// Create filter based on class and id
$classFilter = sprintf(
"(\"File\".\"ClassName\" IN (%s))",
implode(", ", Convert::raw2sql($fileClasses, true))
);
if ($usedFiles) {
return "\"File\".\"ID\" NOT IN (" . implode(', ', $usedFiles) . ") AND $classFilter";
} else {
return $classFilter;
}
}
}

View File

@ -7,8 +7,10 @@ use SilverStripe\ORM\DataObject;
/**
* Represents a link between a dataobject parent and a page in a HTML content area
*
* @method SiteTree Linked()
* @method DataObject Parent()
* @method DataObject Parent() Parent object
* @method SiteTree Linked() Page being linked to
*
* Run `MigrateSiteTreeLinkingTask` to migrate from old table to this.
*/
class SiteTreeLink extends DataObject
{

View File

@ -27,9 +27,7 @@ use SilverStripe\View\Parsers\HTMLValue;
* field to your `db` config and this extension will ensure it's flagged appropriately.
*
* @property DataObject|SiteTreeLinkTracking $owner
* @method ManyManyThroughList<SiteTree> LinkTracking()
*
* @extends DataExtension<DataObject>
* @method ManyManyThroughList LinkTracking() List of site pages linked on this dataobject
*/
class SiteTreeLinkTracking extends DataExtension
{

View File

@ -61,6 +61,7 @@ class SiteTreeLinkTracking_Parser
$matches = [];
if (preg_match('/\[sitetree_link(?:\s*|%20|,)?id=(?<id>[0-9]+)\](#(?<anchor>.*))?/i', $href ?? '', $matches)) {
// Check if page link is broken
/** @var SiteTree $page */
$page = DataObject::get_by_id(SiteTree::class, $matches['id']);
if (!$page) {
// Page doesn't exist.

View File

@ -4,6 +4,7 @@ namespace SilverStripe\CMS\Model;
use Page;
use SilverStripe\Core\Convert;
use SilverStripe\Dev\Deprecation;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\ReadonlyTransformation;
@ -21,8 +22,8 @@ use SilverStripe\View\HTML;
*
* Note: This Only duplicates $db fields and not the $has_one etc..
*
* @property int $CopyContentFromID
* @method SiteTree CopyContentFrom()
* @property int $CopyContentFromID
*/
class VirtualPage extends Page
{
@ -359,6 +360,30 @@ class VirtualPage extends Page
return $result;
}
/**
* @deprecated 4.2.0 Will be removed without equivalent functionality to replace it
*/
public function updateImageTracking()
{
Deprecation::notice('4.2.0', 'Will be removed without equivalent functionality to replace it');
// Doesn't work on unsaved records
if (!$this->isInDB()) {
return;
}
// Remove CopyContentFrom() from the cache
unset($this->components['CopyContentFrom']);
// Update ImageTracking
$copyContentFrom = $this->CopyContentFrom();
if (!$copyContentFrom || !$copyContentFrom->isInDB()) {
return;
}
$this->FileTracking()->setByIDList($copyContentFrom->FileTracking()->column('ID'));
}
public function CMSTreeClasses()
{
$parentClass = sprintf(

View File

@ -46,7 +46,7 @@ class BrokenFilesReport extends Report
{
return [
"Title" => [
"title" => "Title",
"title" => "Title", // todo: use NestedTitle(2)
"link" => true,
],
];

View File

@ -39,7 +39,7 @@ class BrokenRedirectorPagesReport extends Report
{
return [
"Title" => [
"title" => "Title",
"title" => "Title", // todo: use NestedTitle(2)
"link" => true,
],
];

View File

@ -39,7 +39,7 @@ class BrokenVirtualPagesReport extends Report
{
return [
"Title" => [
"title" => "Title",
"title" => "Title", // todo: use NestedTitle(2)
"link" => true,
],
];

View File

@ -9,6 +9,7 @@ use SilverStripe\Reports\Report;
class EmptyPagesReport extends Report
{
public function title()
{
return _t(__CLASS__.'.EMPTYPAGES', "Pages without content");
@ -28,7 +29,7 @@ class EmptyPagesReport extends Report
* Gets the source records
*
* @param array $params
* @return DataList<SiteTree>
* @return DataList
*/
public function sourceRecords($params = null)
{
@ -42,7 +43,7 @@ class EmptyPagesReport extends Report
{
return [
"Title" => [
"title" => "Title",
"title" => "Title", // todo: use NestedTitle(2)
"link" => true,
],
];

View File

@ -31,14 +31,14 @@ class RecentlyEditedReport extends Report
$threshold = strtotime('-14 days', DBDatetime::now()->getTimestamp());
return SiteTree::get()
->filter('LastEdited:GreaterThan', date("Y-m-d H:i:s", $threshold))
->orderBy("\"$tableName\".\"LastEdited\" DESC");
->sort("\"$tableName\".\"LastEdited\" DESC");
}
public function columns()
{
return [
"Title" => [
"title" => "Title",
"title" => "Title", // todo: use NestedTitle(2)
"link" => true,
],
];

View File

@ -2,7 +2,6 @@
namespace SilverStripe\CMS\Search;
use SilverStripe\CMS\Controllers\ContentController;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\Extension;
use SilverStripe\Forms\TextField;
@ -13,8 +12,6 @@ use SilverStripe\ORM\Search\FulltextSearchable;
/**
* Extension to provide a search interface when applied to ContentController
*
* @extends Extension<ContentController>
*/
class ContentControllerSearchExtension extends Extension
{
@ -42,6 +39,7 @@ class ContentControllerSearchExtension extends Extension
$actions = FieldList::create(
FormAction::create('results', _t('SilverStripe\\CMS\\Search\\SearchForm.GO', 'Go'))
);
/** @skipUpgrade */
$form = SearchForm::create($this->owner, 'SearchForm', $fields, $actions);
$form->classesToSearch(FulltextSearchable::get_searchable_classes());
return $form;

View File

@ -13,11 +13,17 @@ use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\SS_List;
use Translatable;
/**
* Standard basic search form which conducts a fulltext search on all {@link SiteTree}
* objects.
*
* If multilingual content is enabled through the {@link Translatable} extension,
* only pages the currently set language on the holder for this searchform are found.
* The language is set through a hidden field in the form, which is prepoluated
* with {@link Translatable::get_current_locale()} when then form is constructed.
*
* @see Use ModelController and SearchContext for a more generic search implementation based around DataObject
*/
class SearchForm extends Form
@ -45,6 +51,7 @@ class SearchForm extends Form
];
/**
* @skipUpgrade
* @param RequestHandler $controller
* @param string $name The name of the form (used in URL addressing)
* @param FieldList $fields Optional, defaults to a single field named "Search". Search logic needs to be customized
@ -63,6 +70,12 @@ class SearchForm extends Form
);
}
if (class_exists('Translatable')
&& SiteTree::singleton()->hasExtension('Translatable')
) {
$fields->push(new HiddenField('searchlocale', 'searchlocale', Translatable::get_current_locale()));
}
if (!$actions) {
$actions = new FieldList(
new FormAction("results", _t(__CLASS__.'.GO', 'Go'))
@ -117,6 +130,22 @@ class SearchForm extends Form
// Get request data from request handler
$request = $this->getRequestHandler()->getRequest();
// set language (if present)
$locale = null;
$origLocale = null;
if (class_exists('Translatable')) {
$locale = $request->requestVar('searchlocale');
if (SiteTree::singleton()->hasExtension('Translatable') && $locale) {
if ($locale === "ALL") {
Translatable::disable_locale_filter();
} else {
$origLocale = Translatable::get_current_locale();
Translatable::set_current_locale($locale);
}
}
}
$keywords = $request->requestVar('Search');
$andProcessor = function ($matches) {
@ -152,6 +181,17 @@ class SearchForm extends Form
}
}
// reset locale
if (class_exists('Translatable')) {
if (SiteTree::singleton()->hasExtension('Translatable') && $locale) {
if ($locale == "ALL") {
Translatable::enable_locale_filter();
} else {
Translatable::set_current_locale($origLocale);
}
}
}
return $results;
}

View File

@ -0,0 +1,70 @@
<?php
namespace SilverStripe\CMS\Tasks;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Dev\BuildTask;
use SilverStripe\Dev\Debug;
use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\Versioned\Versioned;
/**
* Updates legacy SiteTree link tracking into new polymorphic many_many relation.
* This should be done for any site upgrading to 4.2.0
*
* @deprecated 4.13.0 Will be removed without equivalent functionality to replace it
*/
class MigrateSiteTreeLinkingTask extends BuildTask
{
private static $segment = 'MigrateSiteTreeLinkingTask';
protected $title = 'Migrate SiteTree Linking Task';
protected $description = 'Updates legacy SiteTree link tracking into new polymorphic many_many relation';
public function __construct()
{
Deprecation::notice(
'4.13.0',
'Will be removed without equivalent functionality to replace it',
Deprecation::SCOPE_CLASS
);
}
public function run($request)
{
// Ensure legacy table exists
$exists = DB::get_conn()->getSchemaManager()->hasTable('SiteTree_LinkTracking');
if (!$exists) {
DB::alteration_message("Table SiteTree_LinkTracking has already been migrated, or doesn't exist");
return;
}
$pages = 0;
// Ensure sync occurs on draft
Versioned::withVersionedMode(function () use (&$pages) {
Versioned::set_stage(Versioned::DRAFT);
$sitetreeTbl = DataObject::singleton(SiteTree::class)->baseTable();
/** @var SiteTree[] $linkedPages */
$linkedPages = SiteTree::get()
->innerJoin(
'SiteTree_LinkTracking',
"\"SiteTree_LinkTracking\".\"SiteTreeID\" = \"$sitetreeTbl\".\"ID\""
);
foreach ($linkedPages as $page) {
// Command page to update symlink tracking
$page->syncLinkTracking();
$pages++;
}
});
DB::alteration_message("Migrated page links on " . SiteTree::singleton()->i18n_pluralise($pages));
// Disable table to prevent double-migration
DB::dont_require_table('SiteTree_LinkTracking');
}
}

View File

@ -0,0 +1,388 @@
<?php
namespace SilverStripe\CMS\Tasks;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\Controller;
use SilverStripe\Forms\CheckboxSetField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HeaderField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\OptionsetField;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\SS_List;
use SilverStripe\Versioned\Versioned;
use SilverStripe\Security\Permission;
use SilverStripe\Security\Security;
use SilverStripe\View\Requirements;
use SilverStripe\Dev\Deprecation;
/**
* Identify "orphaned" pages which point to a parent
* that no longer exists in a specific stage.
* Shows the pages to an administrator, who can then
* decide which pages to remove by ticking a checkbox
* and manually executing the removal.
*
* Caution: Pages also count as orphans if they don't
* have parents in this stage, even if the parent has a representation
* in the other stage:
* - A live child is orphaned if its parent was deleted from live, but still exists on stage
* - A stage child is orphaned if its parent was deleted from stage, but still exists on live
*
* See {@link RemoveOrphanedPagesTaskTest} for an example sitetree
* before and after orphan removal.
*
* @author Ingo Schommer (<firstname>@silverstripe.com), SilverStripe Ltd.
*
* @deprecated 4.13.0 Will be removed without equivalent functionality to replace it
*/
class RemoveOrphanedPagesTask extends Controller
{
private static $allowed_actions = [
'index' => 'ADMIN',
'Form' => 'ADMIN',
'run' => 'ADMIN',
'handleAction' => 'ADMIN',
];
protected $title = 'Removed orphaned pages without existing parents from both stage and live';
protected $description = "
<p>
Identify 'orphaned' pages which point to a parent
that no longer exists in a specific stage.
</p>
<p>
Caution: Pages also count as orphans if they don't
have parents in this stage, even if the parent has a representation
in the other stage:<br />
- A live child is orphaned if its parent was deleted from live, but still exists on stage<br />
- A stage child is orphaned if its parent was deleted from stage, but still exists on live
</p>
";
protected $orphanedSearchClass = SiteTree::class;
public function __construct()
{
Deprecation::notice(
'4.13.0',
'Will be removed without equivalent functionality to replace it',
Deprecation::SCOPE_CLASS
);
}
protected function init()
{
parent::init();
if (!Permission::check('ADMIN')) {
Security::permissionFailure($this);
}
}
public function Link($action = null)
{
/** @skipUpgrade */
return Controller::join_links('RemoveOrphanedPagesTask', $action, '/');
}
public function index()
{
Requirements::javascript('http://code.jquery.com/jquery-1.7.2.min.js');
Requirements::customCSS('#OrphanIDs .middleColumn {width: auto;}');
Requirements::customCSS('#OrphanIDs label {display: inline;}');
return $this->renderWith('BlankPage');
}
public function Form()
{
$fields = new FieldList();
$source = [];
$fields->push(new HeaderField(
'Header',
_t(__CLASS__ . '.HEADER', 'Remove all orphaned pages task')
));
$fields->push(new LiteralField(
'Description',
$this->description
));
$orphans = $this->getOrphanedPages($this->orphanedSearchClass);
if ($orphans) {
foreach ($orphans as $orphan) {
/** @var SiteTree $latestVersion */
$latestVersion = Versioned::get_latest_version($this->orphanedSearchClass, $orphan->ID);
$latestAuthor = DataObject::get_by_id('SilverStripe\\Security\\Member', $latestVersion->AuthorID);
$orphanBaseTable = DataObject::getSchema()->baseDataTable($this->orphanedSearchClass);
$liveRecord = Versioned::get_one_by_stage(
$this->orphanedSearchClass,
'Live',
["\"$orphanBaseTable\".\"ID\"" => $orphan->ID]
);
$label = sprintf(
'<a href="admin/pages/edit/show/%d">%s</a> <small>(#%d, Last Modified Date: %s, Last Modifier: %s, %s)</small>',
$orphan->ID,
$orphan->Title,
$orphan->ID,
$orphan->dbObject('LastEdited')->Nice(),
($latestAuthor) ? $latestAuthor->Title : 'unknown',
($liveRecord) ? 'is published' : 'not published'
);
$source[$orphan->ID] = $label;
}
}
if ($orphans && $orphans->count()) {
$fields->push(new CheckboxSetField('OrphanIDs', false, $source));
$fields->push(new LiteralField(
'SelectAllLiteral',
sprintf(
'<p><a href="#" onclick="javascript:jQuery(\'#Form_Form_OrphanIDs :checkbox\').attr(\'checked\', \'checked\'); return false;">%s</a>&nbsp;',
_t(__CLASS__ . '.SELECTALL', 'select all')
)
));
$fields->push(new LiteralField(
'UnselectAllLiteral',
sprintf(
'<a href="#" onclick="javascript:jQuery(\'#Form_Form_OrphanIDs :checkbox\').attr(\'checked\', \'\'); return false;">%s</a></p>',
_t(__CLASS__ . '.UNSELECTALL', 'unselect all')
)
));
$fields->push(new OptionsetField(
'OrphanOperation',
_t('SilverStripe\\CMS\\Tasks\\RemoveOrphanedPagesTask.CHOOSEOPERATION', 'Choose operation:'),
[
'rebase' => _t(
__CLASS__ . '.OPERATION_REBASE',
sprintf(
'Rebase selected to a new holder page "%s" and unpublish. None of these pages will show up for website visitors.',
$this->rebaseHolderTitle()
)
),
'remove' => _t(__CLASS__ . '.OPERATION_REMOVE', 'Remove selected from all stages (WARNING: Will destroy all selected pages from both stage and live)'),
],
'rebase'
));
$fields->push(new LiteralField(
'Warning',
sprintf(
'<p class="message">%s</p>',
_t(
__CLASS__ . '.DELETEWARNING',
'Warning: These operations are not reversible. Please handle with care.'
)
)
));
} else {
$fields->push(new LiteralField(
'NotFoundLabel',
sprintf(
'<p class="message">%s</p>',
_t(__CLASS__ . '.NONEFOUND', 'No orphans found')
)
));
}
$form = new Form(
$this,
'SilverStripe\\Forms\\Form',
$fields,
new FieldList(
new FormAction('doSubmit', _t(__CLASS__ . '.BUTTONRUN', 'Run'))
)
);
if (!$orphans || !$orphans->count()) {
$form->makeReadonly();
}
return $form;
}
public function run($request)
{
// @todo Merge with BuildTask functionality
}
public function doSubmit($data, $form)
{
set_time_limit(60*10); // 10 minutes
if (!isset($data['OrphanIDs']) || !isset($data['OrphanOperation'])) {
return false;
}
$successIDs = null;
switch ($data['OrphanOperation']) {
case 'remove':
$successIDs = $this->removeOrphans($data['OrphanIDs']);
break;
case 'rebase':
$successIDs = $this->rebaseOrphans($data['OrphanIDs']);
break;
default:
throw new \InvalidArgumentException(sprintf("Unknown operation: '%s'", $data['OrphanOperation']));
}
$content = '';
if ($successIDs) {
$content .= "<ul>";
foreach ($successIDs as $id => $label) {
$content .= sprintf('<li>%s</li>', $label);
}
$content .= "</ul>";
} else {
$content = _t(__CLASS__ . '.NONEREMOVED', 'None removed');
}
return $this->customise([
'Content' => $content,
'Form' => ' '
])->renderWith('BlankPage');
}
protected function removeOrphans($orphanIDs)
{
$removedOrphans = [];
$orphanBaseTable = DataObject::getSchema()->baseDataTable($this->orphanedSearchClass);
foreach ($orphanIDs as $id) {
/** @var SiteTree $stageRecord */
$stageRecord = Versioned::get_one_by_stage(
$this->orphanedSearchClass,
Versioned::DRAFT,
["\"$orphanBaseTable\".\"ID\"" => $id]
);
if ($stageRecord) {
$removedOrphans[$stageRecord->ID] = sprintf('Removed %s (#%d) from Stage', $stageRecord->Title, $stageRecord->ID);
$stageRecord->delete();
$stageRecord->destroy();
unset($stageRecord);
}
/** @var SiteTree $liveRecord */
$liveRecord = Versioned::get_one_by_stage(
$this->orphanedSearchClass,
Versioned::LIVE,
["\"$orphanBaseTable\".\"ID\"" => $id]
);
if ($liveRecord) {
$removedOrphans[$liveRecord->ID] = sprintf('Removed %s (#%d) from Live', $liveRecord->Title, $liveRecord->ID);
$liveRecord->doUnpublish();
$liveRecord->destroy();
unset($liveRecord);
}
}
return $removedOrphans;
}
protected function rebaseHolderTitle()
{
return sprintf('Rebased Orphans (%s)', date('d/m/Y g:ia', time()));
}
protected function rebaseOrphans($orphanIDs)
{
$holder = new SiteTree();
$holder->ShowInMenus = 0;
$holder->ShowInSearch = 0;
$holder->ParentID = 0;
$holder->Title = $this->rebaseHolderTitle();
$holder->write();
$removedOrphans = [];
$orphanBaseTable = DataObject::getSchema()->baseDataTable($this->orphanedSearchClass);
foreach ($orphanIDs as $id) {
/** @var SiteTree $stageRecord */
$stageRecord = Versioned::get_one_by_stage(
$this->orphanedSearchClass,
'Stage',
["\"$orphanBaseTable\".\"ID\"" => $id]
);
if ($stageRecord) {
$removedOrphans[$stageRecord->ID] = sprintf('Rebased %s (#%d)', $stageRecord->Title, $stageRecord->ID);
$stageRecord->ParentID = $holder->ID;
$stageRecord->ShowInMenus = 0;
$stageRecord->ShowInSearch = 0;
$stageRecord->write();
$stageRecord->doUnpublish();
$stageRecord->destroy();
//unset($stageRecord);
}
/** @var SiteTree $liveRecord */
$liveRecord = Versioned::get_one_by_stage(
$this->orphanedSearchClass,
'Live',
["\"$orphanBaseTable\".\"ID\"" => $id]
);
if ($liveRecord) {
$removedOrphans[$liveRecord->ID] = sprintf('Rebased %s (#%d)', $liveRecord->Title, $liveRecord->ID);
$liveRecord->ParentID = $holder->ID;
$liveRecord->ShowInMenus = 0;
$liveRecord->ShowInSearch = 0;
$liveRecord->write();
if (!$stageRecord) {
$liveRecord->doRestoreToStage();
}
$liveRecord->doUnpublish();
$liveRecord->destroy();
unset($liveRecord);
}
if ($stageRecord) {
unset($stageRecord);
}
}
return $removedOrphans;
}
/**
* Gets all orphans from "Stage" and "Live" stages.
*
* @param string $class
* @param array $filter
* @param string $sort
* @param string $join
* @param int|array $limit
* @return SS_List
*/
public function getOrphanedPages($class = SiteTree::class, $filter = [], $sort = null, $join = null, $limit = null)
{
// Alter condition
$table = DataObject::getSchema()->tableName($class);
if (empty($filter)) {
$where = [];
} elseif (is_array($filter)) {
$where = $filter;
} else {
$where = [$filter];
}
$where[] = ["\"{$table}\".\"ParentID\" != ?" => 0];
$where[] = '"Parents"."ID" IS NULL';
$orphans = new ArrayList();
foreach ([Versioned::DRAFT, Versioned::LIVE] as $stage) {
$table .= ($stage == Versioned::LIVE) ? '_Live' : '';
$stageOrphans = Versioned::get_by_stage(
$class,
$stage,
$where,
$sort,
null,
$limit
)->leftJoin($table, "\"$table\".\"ParentID\" = \"Parents\".\"ID\"", "Parents");
$orphans->merge($stageOrphans);
}
$orphans->removeDuplicates();
return $orphans;
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace SilverStripe\CMS\Tasks;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\Controller;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\DataObject;
use SilverStripe\Dev\Deprecation;
/**
* @deprecated 4.13.0 Will be removed without equivalent functionality to replace it
*/
class SiteTreeMaintenanceTask extends Controller
{
private static $allowed_actions = [
'*' => 'ADMIN'
];
public function __construct()
{
Deprecation::notice(
'4.13.0',
'Will be removed without equivalent functionality to replace it',
Deprecation::SCOPE_CLASS
);
}
public function makelinksunique()
{
$table = DataObject::singleton(SiteTree::class)->baseTable();
$badURLs = "'" . implode("', '", DB::query("SELECT \"URLSegment\", count(*) FROM \"$table\" GROUP BY \"URLSegment\" HAVING count(*) > 1")->column()) . "'";
$pages = DataObject::get(SiteTree::class, "\"$table\".\"URLSegment\" IN ($badURLs)");
foreach ($pages as $page) {
echo "<li>$page->Title: ";
$urlSegment = $page->URLSegment;
$page->write();
if ($urlSegment != $page->URLSegment) {
echo _t(
'SilverStripe\\CMS\\Model\\SiteTree.LINKSCHANGEDTO',
" changed {url1} -> {url2}",
['url1' => $urlSegment, 'url2' => $page->URLSegment]
);
} else {
echo _t(
'SilverStripe\\CMS\\Model\\SiteTree.LINKSALREADYUNIQUE',
" {url} is already unique",
['url' => $urlSegment]
);
}
die();
}
}
public function Link($action = null)
{
/** @skipUpgrade */
return Controller::join_links('SiteTreeMaintenanceTask', $action, '/');
}
}

View File

@ -19,21 +19,19 @@
}
],
"require": {
"php": "^8.1",
"silverstripe/admin": "^2.2",
"silverstripe/campaign-admin": "^2",
"silverstripe/framework": "^5.2",
"silverstripe/reports": "^5",
"silverstripe/siteconfig": "^5",
"silverstripe/versioned": "^2",
"silverstripe/versioned-admin": "^2",
"silverstripe/vendor-plugin": "^2"
"silverstripe/admin": "^1.13.20",
"silverstripe/campaign-admin": "^1.7@dev",
"silverstripe/framework": "^4.11",
"silverstripe/reports": "^4.7@dev",
"silverstripe/siteconfig": "^4.7@dev",
"silverstripe/versioned": "^1.7@dev",
"silverstripe/versioned-admin": "^1.7@dev",
"silverstripe/vendor-plugin": "^1.0",
"php": "^7.4 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^9.6",
"squizlabs/php_codesniffer": "^3.7",
"silverstripe/standards": "^1",
"phpstan/extension-installer": "^1.3"
"phpunit/phpunit": "^9.5",
"squizlabs/php_codesniffer": "^3"
},
"extra": {
"expose": [

View File

@ -12,10 +12,10 @@ en:
UNPUBLISHED_PAGES: 'Unpublished %d pages'
UNPUBLISH_PAGES: Unpublish
SilverStripe\CMS\Controllers\CMSMain:
ACCESS: "Access to '{title}' section"
ACCESS: 'Access to ''{title}'' section'
ACCESS_HELP: 'Allow viewing of the section containing page tree and content. View and edit permissions can be handled through page specific dropdowns, as well as the separate "Content permissions".'
ARCHIVE: Archive
ARCHIVEDPAGE: "Archived page '{title}'"
ARCHIVEDPAGE: 'Archived page ''{title}'''
AddNew: 'Add new page'
AddNewButton: 'Add new'
AddPageRestriction: 'Note: Some page types are not allowed for this selection'
@ -28,35 +28,35 @@ en:
ChoosePageParentMode: 'Choose where to create this page'
ChoosePageType: 'Choose page type'
Create: Create
DUPLICATED: "Duplicated '{title}' successfully"
DUPLICATEDWITHCHILDREN: "Duplicated '{title}' and children successfully"
DUPLICATED: 'Duplicated ''{title}'' successfully'
DUPLICATEDWITHCHILDREN: 'Duplicated ''{title}'' and children successfully'
EMAIL: Email
NEWPAGE: 'New {pagetype}'
PAGENOTEXISTS: "This page doesn't exist"
PAGENOTEXISTS: 'This page doesn''t exist'
PAGES: 'Page status'
PAGESALLOPT: 'All pages'
PAGETYPEANYOPT: Any
PAGETYPEOPT: 'Page type'
PAGETYPE_TITLE: '(Page type: {type}) {title}'
PLEASESAVE: "Please Save Page: This page could not be updated because it hasn't been saved yet."
PLEASESAVE: 'Please Save Page: This page could not be updated because it hasn''t been saved yet.'
PUBALLCONFIRM: 'Please publish every page in the site, copying content stage to live'
PUBALLFUN: '"Publish All" functionality'
PUBALLFUN2: 'Pressing this button will do the equivalent of going to every page and pressing "publish". It''s intended to be used after there have been massive edits of the content, such as when the site was first built. For large websites, this task might not be able to run through to completion. In this case, we recommend talking to your developers to create a custom task'
PUBLISHED: "Published '{title}' successfully."
PUBLISHED: 'Published ''{title}'' successfully.'
PUBPAGES: 'Done: Published {count} pages'
PageAdded: 'Successfully created page'
REMOVEDPAGE: "Removed '{title}' from the published site"
REMOVEDPAGEFROMDRAFT: "Removed '{title}' from the draft site"
REMOVEDPAGE: 'Removed ''{title}'' from the published site'
REMOVEDPAGEFROMDRAFT: 'Removed ''{title}'' from the draft site'
REORGANISATIONSUCCESSFUL: 'Reorganised the site tree successfully.'
RESTORE: 'Restore draft'
RESTORED: "Restored '{title}' successfully"
RESTORED: 'Restored ''{title}'' successfully'
RESTORE_DESC: 'Restore the archived version to draft'
RESTORE_TO_ROOT: 'Restore draft at top level'
RESTORE_TO_ROOT_DESC: 'Restore the archived version to draft as a top level page'
ROLLBACK: 'Roll back to this version'
ROLLEDBACKPUBv2: 'Rolled back to published version.'
ROLLEDBACKVERSIONv2: 'Rolled back to version #{version}.'
SAVED: "Saved '{title}' successfully."
SAVED: 'Saved ''{title}'' successfully.'
SAVEDRAFT: Save
SEARCHRESULTS: 'Search results'
SHOW_AS_LIST: 'show as list'
@ -73,8 +73,8 @@ en:
ParentMode_top: 'Top level'
Title: 'Add page'
SilverStripe\CMS\Controllers\CMSPageEditController:
ErrorItemPermissionDenied: "It seems you don't have the necessary permissions to add {ObjectTitle} to a campaign"
ErrorNotFound: "That {Type} couldn't be found"
ErrorItemPermissionDenied: 'It seems you don''t have the necessary permissions to add {ObjectTitle} to a campaign'
ErrorNotFound: 'That {Type} couldn''t be found'
MENUTITLE: 'Edit Page'
SilverStripe\CMS\Controllers\CMSPageHistoryController:
AUTHOR: Author
@ -114,6 +114,7 @@ en:
SilverStripe\CMS\Controllers\CMSSiteTreeFilter_StatusRemovedFromDraftPages:
Title: 'Live but removed from draft'
SilverStripe\CMS\Controllers\ContentController:
ARCHIVEDSITE: 'Preview version'
ARCHIVEDSITEFROM: 'Archived site from'
CMS: CMS
DRAFT: Draft
@ -133,8 +134,11 @@ en:
Password: Password
PostInstallTutorialIntro: 'This website is a simplistic version of a SilverStripe 3 site. To extend this, please take a look at {link}.'
StartEditing: 'You can start editing your content by opening <a href="{link}">the CMS</a>.'
UNVERSIONEDPREVIEW: Preview
UnableDeleteInstall: 'Unable to delete installation files. Please delete the files below manually'
VIEWPAGEIN: 'View Page in:'
SilverStripe\CMS\Controllers\SilverStripeNavigator:
ARCHIVED: Archived
SilverStripe\CMS\Forms\AnchorLinkFormFactory:
ANCHORVALUE: Anchor
SilverStripe\CMS\Forms\InternalLinkFormFactory:
@ -214,7 +218,7 @@ en:
LASTPUBLISHED: 'Last published'
LASTSAVED: 'Last saved'
LASTUPDATED: 'Last Updated'
LINKCHANGENOTE: "Changing this page's link will also affect the links of all child pages."
LINKCHANGENOTE: 'Changing this page''s link will also affect the links of all child pages.'
LINKSALREADYUNIQUE: ' {url} is already unique'
LINKSCHANGEDTO: ' changed {url1} -> {url2}'
MENUTITLE: 'Navigation label'

View File

@ -1,15 +1,15 @@
{
"name": "silverstripe-cms",
"version": "5.0.0",
"version": "4.0.0",
"description": "The SilverStripe CMS",
"directories": {
"test": "tests"
},
"engines": {
"node": "^18.x"
"node": "^10.x"
},
"scripts": {
"build": "yarn && yarn lint && yarn test && rm -rf client/dist/* && NODE_ENV=production webpack --mode production --bail --progress",
"build": "yarn && yarn test && NODE_ENV=production webpack -p --bail --progress",
"dev": "NODE_ENV=development webpack --progress",
"watch": "NODE_ENV=development webpack --watch --progress",
"css": "WEBPACK_CHILD=css npm run build",
@ -32,42 +32,36 @@
},
"homepage": "https://github.com/silverstripe/silverstripe-cms#readme",
"dependencies": {
"@apollo/client": "^3.7.1",
"@popperjs/core": "^2.11.6",
"bootstrap": "^4.6.2",
"classnames": "^2.3.2",
"@silverstripe/reactstrap-confirm": "0.0.5",
"apollo-client": "^2.4.2",
"bootstrap": "^4.3.1",
"classnames": "^2.2.5",
"deep-freeze-strict": "^1.1.1",
"graphql": "^16.8.1",
"graphql-tag": "^2.12.6",
"isomorphic-fetch": "^3.0.0",
"graphql": "^14.0.0",
"graphql-tag": "^2.10.0",
"isomorphic-fetch": "^2.2.1",
"jquery": "^3.5.0",
"merge": "^2.1.1",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.5",
"react-select": "^5.6.0",
"reactstrap": "^8.9.0",
"reactstrap-confirm": "^1.3.2",
"redux": "^4.2.0",
"redux-form": "^8.3.8"
"popper.js": "^1.14.4",
"prop-types": "^15.6.2",
"react": "^16.6.1",
"react-apollo": "^2.1.11",
"react-dom": "^16.6.1",
"react-redux": "^5.0.7",
"react-select": "^1.3",
"reactstrap": "^6.4.0",
"redux": "^4.0.0",
"redux-form": "^7.4.2"
},
"devDependencies": {
"@silverstripe/eslint-config": "^1.1.0",
"@silverstripe/webpack-config": "^2.0.0",
"@testing-library/react": "^14.0.0",
"babel-jest": "^29.3.0",
"copy-webpack-plugin": "^11.0.0",
"core-js": "^3.26.0",
"jest-cli": "^29.3.0",
"jest-environment-jsdom": "^29.3.1",
"webpack": "^5.74.0",
"webpack-cli": "^5.0.0"
"@silverstripe/eslint-config": "^0.1.0",
"@silverstripe/webpack-config": "^1.3.0",
"babel-jest": "^23.6.0",
"babel-polyfill": "^6.26.0",
"copy-webpack-plugin": "^4",
"jest-cli": "^23.6.0"
},
"browserslist": [
"defaults"
],
"jest": {
"testEnvironment": "jsdom",
"roots": [
"client/src"
],
@ -88,6 +82,16 @@
}
},
"resolutions": {
"colors": "1.4.0"
"colors": "1.1.2",
"eslint": "^4.6.1"
},
"babel": {
"presets": [
"env",
"react"
],
"plugins": [
"transform-object-rest-spread"
]
}
}

View File

@ -1,3 +0,0 @@
parameters:
paths:
- code

0
silverstripe_version Normal file
View File

View File

@ -0,0 +1,16 @@
<div class="cms-preview fill-height flexbox-area-grow" data-layout-type="border">
<div class="panel flexbox-area-grow fill-height">
<div class="preview-note">
<div class="icon font-icon-monitor display-1"></div>
<%t SilverStripe\CMS\Controllers\CMSPageHistoryController.NO_PREVIEW 'No preview available' %>
</div>
<div class="preview__device">
<div class="preview-device-outer">
<div class="preview-device-inner">
<iframe src="about:blank" class="center" name="cms-preview-iframe"></iframe>
</div>
</div>
</div>
</div>
<div class="toolbar toolbar--south cms-content-controls cms-preview-controls"></div>
</div>

View File

@ -13,12 +13,12 @@
<div class="input-group">
<input $AttributesHTML />
<div class="input-group-append">
<button role="button" type="button" class="btn btn-primary update">
<button role="button" data-icon="accept" type="button" class="btn btn-primary update">
<%t SilverStripe\CMS\Forms\SiteTreeURLSegmentField.OK 'OK' %>
</button>
</div>
<div class="input-group-append">
<button role="button" type="button" class="btn btn-outline-secondary btn-sm input-group-append cancel">
<button role="button" data-icon="cancel" type="button" class="btn btn-outline-secondary btn-sm input-group-append cancel">
<%t SilverStripe\CMS\Forms\SiteTreeURLSegmentField.Cancel 'Cancel' %>
</button>
</div>

View File

@ -0,0 +1,27 @@
<% if $BackLinkTracking %>
<table class="table">
<thead>
<tr>
<th><%t SilverStripe\CMS\Model\SiteTreeFileExtension.TITLE_INDEX '#' %></th>
<th><%t SilverStripe\CMS\Model\SiteTreeFileExtension.TITLE_USED_ON 'Used on' %></th>
<th><%t SilverStripe\CMS\Model\SiteTreeFileExtension.TITLE_TYPE 'Type' %></th>
</tr>
</thead>
<tbody>
<% loop $BackLinkTracking %>
<tr>
<td>$Pos</td>
<td><a href="$CMSEditLink">$MenuTitle</a></td>
<td>
$i18n_singular_name
<% if $isPublished %>
<span class="badge badge-success">Published</span>
<% else %>
<span class="badge status-addedtodraft">Draft</span>
<% end_if %>
</td>
</tr>
<% end_loop %>
</tbody>
</table>
<% end_if %>

View File

@ -64,7 +64,7 @@ Feature: Edit a page
Then I should see "About Us"
And I go to "/about-modified-us"
Then I should not see "About Us"
# Assert URL segment + metadata on frontend
When I go to "/admin/pages"
And I click on "About Us" in the tree
@ -81,19 +81,19 @@ Feature: Edit a page
When I click on "About Us" in the tree
# Embed files from the "Files" section of the admin area
And I press the "Insert from Files" HTML field button
And I click on the "div[aria-label='Insert from Files'] button" element
And I click on the ".gallery__files .gallery-item__thumbnail" element
And I press the "Insert file" button
# Link to a file in the "Files" section of the admin area
And I press the "Insert link" HTML field button
And I click "Link to a file" in the ".tox-collection__group" element
And I click on the "div[aria-label='Insert link [Ctrl+K]'] button" element
And I select "Link to a file" from the TinyMCE menu with javascript
And I click on the ".gallery__files .gallery-item__thumbnail" element
And I fill in "Form_fileInsertForm_Text" with "MyImage"
And I press the "Link to file" button
# Embed media from a URL
And I press the "Insert media via URL" button
And I click on the "div[aria-label='Insert media via URL'] button" element
And I fill in "Form_remoteCreateForm_Url" with "https://www.youtube.com/watch?v=ScMzIvxBSi4"
And I press "Add media"
And I wait for 15 seconds

View File

@ -17,11 +17,12 @@ So that I can link to a external website or a page on my site
Scenario: I can link to an internal page
When I select "awesome" in the "Content" HTML field
And I press the "Insert link" HTML field button
And I click "Page on this site" in the ".tox-collection__group" element
And I click "Page on this site" in the ".mce-menu" element
Then I should see an "form#Form_editorInternalLink" element
When I select "About Us" in the "#Form_editorInternalLink_PageID_Holder" tree dropdown
When I click "(Search or choose Page)" in the ".Select-multi-value-wrapper" element
And I click "About Us" in the ".treedropdownfield__menu" element
And I fill in "my desc" for "Link description"
And I press the "Insert link" button
And I press the "Insert" button
Then the "Content" HTML field should contain "<a title="my desc" href="[sitetree_link,id=2]">awesome</a>"
# Required to avoid "unsaved changes" browser dialog
Then I press the "Save" button
@ -30,11 +31,12 @@ So that I can link to a external website or a page on my site
Given I fill in the "Content" HTML field with "<p><img src='file1.jpg'></p>"
When I select the image "file1.jpg" in the "Content" HTML field
And I press the "Insert link" HTML field button
And I click "Page on this site" in the ".tox-collection__group" element
And I click "Page on this site" in the ".mce-menu" element
Then I should see an "form#Form_editorInternalLink" element
And I should not see "Link text"
When I select "About Us" in the "#Form_editorInternalLink_PageID_Holder" tree dropdown
And I press the "Insert link" button
When I click "(Search or choose Page)" in the ".Select-multi-value-wrapper" element
And I click "About Us" in the ".treedropdownfield__menu" element
And I press the "Insert" button
Then the "Content" HTML field should contain "<a href="[sitetree_link,id=2]"><img src="file1.jpg"></a>"
# Required to avoid "unsaved changed" browser dialog
And I press the "Save" button
@ -43,14 +45,15 @@ So that I can link to a external website or a page on my site
Given I fill in the "Content" HTML field with "<a title='my desc' href='[sitetree_link,id=2]'>awesome</a>"
And I select "awesome" in the "Content" HTML field
And I press the "Insert link" HTML field button
And I click "Page on this site" in the ".tox-collection__group" element
And I click "Page on this site" in the ".mce-menu" element
And I should see an "form#Form_editorInternalLink" element
Then I should see "About Us" in the "#Form_editorInternalLink_PageID_Holder .treedropdownfield__value-container" element
Then I should see "About Us" in the ".Select-value" element
And the "Link description" field should contain "my desc"
# This doesn't seem to suffer from that issue
When I select "Home" in the "#Form_editorInternalLink_PageID_Holder" tree dropdown
When I click "About Us" in the ".Select-value" element
And I click "Home" in the ".treedropdownfield__menu" element
And I fill in "my new desc" for "Link description"
And I press the "Insert link" button
And I press the "Insert" button
Then the "Content" HTML field should contain "<a title="my new desc" href="[sitetree_link,id=1]">awesome</a>"
# Required to avoid "unsaved changes" browser dialog
Then I press the "Save" button
@ -58,11 +61,11 @@ So that I can link to a external website or a page on my site
Scenario: I can link to an external URL
Given I select "awesome" in the "Content" HTML field
And I press the "Insert link" HTML field button
When I click "Link to external URL" in the ".tox-collection__group" element
When I click "Link to external URL" in the ".mce-menu" element
And I should see an "form#Form_ModalsEditorExternalLink" element
When I fill in "http://silverstripe.org" for "URL"
And I check "Open in new window/tab"
And I press the "Insert link" button
And I press the "Insert" button
Then the "Content" HTML field should contain "<a rel="noopener" href="http://silverstripe.org" target="_blank">awesome</a>"
# Required to avoid "unsaved changes" browser dialog
Then I press the "Save" button
@ -71,11 +74,11 @@ So that I can link to a external website or a page on my site
Given I fill in the "Content" HTML field with "<p><img src='file1.jpg'></p>"
When I select the image "file1.jpg" in the "Content" HTML field
And I press the "Insert link" HTML field button
When I click "Link to external URL" in the ".tox-collection__group" element
When I click "Link to external URL" in the ".mce-menu" element
And I should see an "form#Form_ModalsEditorExternalLink" element
And I should not see "Link text"
When I fill in "http://silverstripe.org" for "URL"
And I press the "Insert link" button
And I press the "Insert" button
Then the "Content" HTML field should contain "<a href="http://silverstripe.org"><img src="file1.jpg"></a>"
# Required to avoid "unsaved changed" browser dialog
And I press the "Save" button
@ -84,12 +87,12 @@ So that I can link to a external website or a page on my site
Given I fill in the "Content" HTML field with "<p>My <a href='http://silverstripe.org'>awesome</a> content"
And I select "awesome" in the "Content" HTML field
When I press the "Insert link" HTML field button
And I click "Link to external URL" in the ".tox-collection__group" element
And I click "Link to external URL" in the ".mce-menu" element
And I should see an "form#Form_ModalsEditorExternalLink" element
Then the "URL" field should contain "http://silverstripe.org"
# This doesn't seem to suffer from that issue
When I fill in "http://google.com" for "URL"
And I press the "Insert link" button
And I press the "Insert" button
Then the "Content" HTML field should contain "<a href="http://google.com">awesome</a>"
# Required to avoid "unsaved changes" browser dialog
Then I press the "Save" button

View File

@ -17,14 +17,16 @@ So that I can link to a external website or a page on my site
Scenario: I can link to an anchor in an internal page
When I select "awesome" in the "Content" HTML field
And I press the "Insert link" HTML field button
And I click "Anchor on a page" in the ".tox-collection__group" element
And I click "Anchor on a page" in the ".mce-menu" element
Then I should see an "form#Form_editorAnchorLink" element
And I should see "About Us" in the "#Form_editorAnchorLink_PageID_Holder .treedropdownfield__value-container" element
When I select "Details" in the "#Form_editorAnchorLink_PageID_Holder" tree dropdown
And I select "youranchor" in the "#Form_editorAnchorLink_Anchor_Holder" anchor dropdown
Then I should see "youranchor" in the "#Form_editorAnchorLink_Anchor_Holder .anchorselectorfield__value-container" element
And I should see "About Us" in the "#Form_editorAnchorLink_PageID_Holder .Select-multi-value-wrapper" element
When I click "About Us" in the "#Form_editorAnchorLink_PageID_Holder .Select-multi-value-wrapper" element
And I click "Details" in the "#Form_editorAnchorLink_PageID_Holder .Select-menu-outer" element
And I click "Select or enter anchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-multi-value-wrapper" element
And I click "youranchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-menu-outer" element
Then I should see "youranchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-value" element
When I fill in "my desc" for "Link description"
And I press the "Insert link" button
And I press the "Insert" button
Then the "Content" HTML field should contain "<a title="my desc" href="[sitetree_link,id=3]#youranchor">awesome</a>"
# Required to avoid "unsaved changes" browser dialog
Then I press the "Save" button
@ -33,13 +35,16 @@ So that I can link to a external website or a page on my site
Given I fill in the "Content" HTML field with "<p><img src='file1.jpg'></p>"
When I select the image "file1.jpg" in the "Content" HTML field
And I press the "Insert link" HTML field button
And I click "Anchor on a page" in the ".tox-collection__group" element
And I click "Anchor on a page" in the ".mce-menu" element
Then I should see an "form#Form_editorAnchorLink" element
And I should not see "Link text"
And I should see "About Us" in the "#Form_editorAnchorLink_PageID_Holder .treedropdownfield__value-container" element
When I select "Details" in the "#Form_editorAnchorLink_PageID_Holder" tree dropdown
And I select "youranchor" in the "#Form_editorAnchorLink_Anchor_Holder" anchor dropdown
And I press the "Insert link" button
And I should see "About Us" in the "#Form_editorAnchorLink_PageID_Holder .Select-multi-value-wrapper" element
When I click "About Us" in the "#Form_editorAnchorLink_PageID_Holder .Select-multi-value-wrapper" element
And I click "Details" in the "#Form_editorAnchorLink_PageID_Holder .Select-menu-outer" element
And I click "Select or enter anchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-multi-value-wrapper" element
And I click "youranchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-menu-outer" element
Then I should see "youranchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-value" element
And I press the "Insert" button
Then the "Content" HTML field should contain "<a href="[sitetree_link,id=3]#youranchor"><img src="file1.jpg"></a>"
# Required to avoid "unsaved changed" browser dialog
And I press the "Save" button
@ -47,13 +52,15 @@ So that I can link to a external website or a page on my site
Scenario: I can link to an anchor from a dataobject on the current page
When I select "awesome" in the "Content" HTML field
And I press the "Insert link" HTML field button
And I click "Anchor on a page" in the ".tox-collection__group" element
And I click "Anchor on a page" in the ".mce-menu" element
Then I should see an "form#Form_editorAnchorLink" element
And I should see "About Us" in the "#Form_editorAnchorLink_PageID_Holder .treedropdownfield__value-container" element
When I select "dataobject-anchor" in the "#Form_editorAnchorLink_Anchor_Holder" anchor dropdown
Then I should see "dataobject-anchor" in the "#Form_editorAnchorLink_Anchor_Holder .anchorselectorfield__value-container" element
And I should see "About Us" in the "#Form_editorAnchorLink_PageID_Holder .Select-multi-value-wrapper" element
When I click "Select or enter anchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-multi-value-wrapper" element
Then I should see "dataobject-anchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-menu-outer" element
When I click "dataobject-anchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-menu-outer" element
Then I should see "dataobject-anchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-value" element
When I fill in "my desc" for "Link description"
And I press the "Insert link" button
And I press the "Insert" button
Then the "Content" HTML field should contain "<a title="my desc" href="[sitetree_link,id=2]#dataobject-anchor">awesome</a>"
# Required to avoid "unsaved changes" browser dialog
Then I press the "Save" button
@ -62,15 +69,16 @@ So that I can link to a external website or a page on my site
Given I fill in the "Content" HTML field with "<p>My awesome content</p><p><a id='unsaved-anchor'></a>unsaved content</p>"
When I select "awesome" in the "Content" HTML field
And I press the "Insert link" HTML field button
And I click "Anchor on a page" in the ".tox-collection__group" element
And I click "Anchor on a page" in the ".mce-menu" element
Then I should see an "form#Form_editorAnchorLink" element
And I should see "About Us" in the "#Form_editorAnchorLink_PageID_Holder .treedropdownfield__value-container" element
When I click on the ".anchorselectorfield__dropdown-indicator" element
Then I should see "dataobject-anchor" in the ".anchorselectorfield__menu-list" element
When I select "unsaved-anchor" in the "#Form_editorAnchorLink_Anchor_Holder" anchor dropdown
Then I should see "unsaved-anchor" in the "#Form_editorAnchorLink_Anchor_Holder .anchorselectorfield__value-container" element
And I should see "About Us" in the "#Form_editorAnchorLink_PageID_Holder .Select-multi-value-wrapper" element
When I click "Select or enter anchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-multi-value-wrapper" element
Then I should see "unsaved-anchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-menu-outer" element
And I should see "dataobject-anchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-menu-outer" element
When I click "unsaved-anchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-menu-outer" element
Then I should see "unsaved-anchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-value" element
When I fill in "my desc" for "Link description"
And I press the "Insert link" button
And I press the "Insert" button
Then the "Content" HTML field should contain "<a title="my desc" href="[sitetree_link,id=2]#unsaved-anchor">awesome</a>"
# Required to avoid "unsaved changes" browser dialog
Then I press the "Save" button

View File

@ -19,6 +19,10 @@ Feature: Preview a page
And the preview contains "About Us"
Then I set the CMS mode to "Edit mode"
# TODO:
# - Only tests correctly on fresh database
# - We should continue testing against it after we have fixtures ready
@javascript
Scenario: I can see an updated preview when editing content
And I go to "/admin/pages"
Then I should see "About Us" in the tree

View File

@ -8,7 +8,6 @@ use Behat\Mink\Element\NodeElement;
use PHPUnit\Framework\Assert;
use SilverStripe\BehatExtension\Context\BasicContext;
use SilverStripe\BehatExtension\Context\FixtureContext as BehatFixtureContext;
use SilverStripe\BehatExtension\Utility\StepHelper;
use SilverStripe\CMS\Model\RedirectorPage;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Core\ClassInfo;
@ -21,8 +20,6 @@ use SilverStripe\Versioned\Versioned;
*/
class FixtureContext extends BehatFixtureContext
{
use StepHelper;
/**
* @var BasicContext
*/
@ -121,6 +118,22 @@ class FixtureContext extends BehatFixtureContext
Assert::assertEquals($value, $radioButton->getAttribute($attribute));
}
/**
* Assumes you've just opened the Insert link menu, e.g.
* I click on the "div[aria-label='Insert link [Ctrl+K]'] button" element
*
* @When /^I select "(.+?)" from the TinyMCE menu with javascript$/
* @param string $label
*/
public function iSelectFromTheTinyMceMenu($label)
{
// :visible and :contains are jQuery css selectors
$js = <<<JS
jQuery(".mce-menu-item:visible span:contains('{$label}')").click();
JS;
$this->getMainContext()->getSession()->executeScript($js);
}
/**
* e.g. --PageOne,--PageTwo,---PageTwoChild,--PageThree
*
@ -142,33 +155,4 @@ JS;
$actual = $this->getMainContext()->getSession()->evaluateScript($js);
Assert::assertEquals($expected, $actual);
}
/**
* Select a value in the anchor selector field
*
* @When /^I select "([^"]*)" in the "([^"]*)" anchor dropdown$/
*/
public function iSelectValueInAnchorDropdown($text, $selector)
{
$page = $this->getMainContext()->getSession()->getPage();
/** @var NodeElement $parentElement */
$parentElement = null;
$this->retryThrowable(function () use (&$parentElement, &$page, $selector) {
$parentElement = $page->find('css', $selector);
Assert::assertNotNull($parentElement, sprintf('"%s" element not found', $selector));
$page = $this->getMainContext()->getSession()->getPage();
});
$this->retryThrowable(function () use ($parentElement, $selector) {
$dropdown = $parentElement->find('css', '.anchorselectorfield__dropdown-indicator');
Assert::assertNotNull($dropdown, sprintf('Unable to find the dropdown in "%s"', $selector));
$dropdown->click();
});
$this->retryThrowable(function () use ($text, $parentElement, $selector) {
$element = $parentElement->find('xpath', sprintf('//*[count(*)=0 and .="%s"]', $text));
Assert::assertNotNull($element, sprintf('"%s" not found in "%s"', $text, $selector));
$element->click();
});
}
}

View File

@ -16,6 +16,7 @@ use SilverStripe\Versioned\Versioned;
*/
class CMSBatchActionsTest extends SapphireTest
{
protected static $fixture_file = 'CMSBatchActionsTest.yml';
protected function setUp(): void
@ -141,10 +142,10 @@ class CMSBatchActionsTest extends SapphireTest
$this->assertEquals($archivedID, $list->first()->ParentID);
// Run restore
$result = json_decode($action->run($list)->getBody(), true);
$result = json_decode($action->run($list) ?? '', true);
$this->assertEquals(
[
$archivedxID => $archivedxID,
$archivedxID => $archivedxID
],
$result['success']
);
@ -161,13 +162,13 @@ class CMSBatchActionsTest extends SapphireTest
$this->assertEquals(0, $list->last()->ParentID); // archived (parent)
// Run restore
$result = json_decode($action->run($list)->getBody(), true);
$result = json_decode($action->run($list) ?? '', true);
$this->assertEquals(
[
// Order of archived is opposite to order items are passed in, as
// these are sorted by level first
$archivedID => $archivedID,
$archivedyID => $archivedyID,
$archivedyID => $archivedyID
],
$result['success']
);

View File

@ -2,6 +2,7 @@
namespace SilverStripe\CMS\Tests\Controllers;
use Page;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Admin\CMSBatchActionHandler;
use SilverStripe\CMS\Controllers\CMSMain;
@ -24,6 +25,7 @@ use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
use SilverStripe\SiteConfig\SiteConfig;
use SilverStripe\Versioned\Versioned;
use SilverStripe\Dev\Deprecation;
class CMSMainTest extends FunctionalTest
{
@ -117,6 +119,40 @@ class CMSMainTest extends FunctionalTest
);
}
/**
* @todo Test the results of a publication better
*/
public function testPublish()
{
if (Deprecation::isEnabled()) {
$this->markTestSkipped('Test calls deprecated code');
}
$page1 = $this->objFromFixture(Page::class, "page1");
$page2 = $this->objFromFixture(Page::class, "page2");
$this->logInAs('admin');
$response = $this->get('admin/pages/publishall?confirm=1');
$this->assertStringContainsString(
'Done: Published 30 pages',
$response->getBody()
);
// Some modules (e.g., cmsworkflow) will remove this action
$actions = CMSBatchActionHandler::config()->batch_actions;
if (isset($actions['publish'])) {
$response = $this->get('admin/pages/batchactions/publish?ajax=1&csvIDs=' . implode(',', [$page1->ID, $page2->ID]));
$responseData = json_decode($response->getBody() ?? '', true);
$this->assertArrayHasKey($page1->ID, $responseData['modified']);
$this->assertArrayHasKey($page2->ID, $responseData['modified']);
}
// Get the latest version of the redirector page
$pageID = $this->idFromFixture(RedirectorPage::class, 'page5');
$latestID = DB::prepared_query('select max("Version") from "RedirectorPage_Versions" where "RecordID" = ?', [$pageID])->value();
$dsCount = DB::prepared_query('select count("Version") from "RedirectorPage_Versions" where "RecordID" = ? and "Version"= ?', [$pageID, $latestID])->value();
$this->assertEquals(1, $dsCount, "Published page has no duplicate version records: it has " . $dsCount . " for version " . $latestID);
}
/**
* Test that getCMSFields works on each page type.
* Mostly, this is just checking that the method doesn't return an error
@ -149,8 +185,8 @@ class CMSMainTest extends FunctionalTest
$this->logInWithPermission('ADMIN');
Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', true);
$parentPage = $this->objFromFixture(SiteTree::class, 'page3');
$childPage = $this->objFromFixture(SiteTree::class, 'page1');
$parentPage = $this->objFromFixture(Page::class, 'page3');
$childPage = $this->objFromFixture(Page::class, 'page1');
$parentPage->doUnpublish();
$childPage->doUnpublish();
@ -172,7 +208,7 @@ class CMSMainTest extends FunctionalTest
$this->logInWithPermission('ADMIN');
// Set up a page that is delete from live
$page = $this->objFromFixture(SiteTree::class, 'page1');
$page = $this->objFromFixture(Page::class, 'page1');
$pageID = $page->ID;
$page->publishRecursive();
$page->delete();
@ -180,7 +216,7 @@ class CMSMainTest extends FunctionalTest
$response = $this->get('admin/pages/edit/show/' . $pageID);
$livePage = Versioned::get_one_by_stage(SiteTree::class, Versioned::LIVE, [
'"SiteTree"."ID"' => $pageID,
'"SiteTree"."ID"' => $pageID
]);
$this->assertInstanceOf(SiteTree::class, $livePage);
$this->assertTrue($livePage->canDelete());
@ -197,7 +233,7 @@ class CMSMainTest extends FunctionalTest
$this->logInWithPermission('ADMIN');
// Set up a page that is delete from live
$page1 = $this->objFromFixture(SiteTree::class, 'page1');
$page1 = $this->objFromFixture(Page::class, 'page1');
$page1ID = $page1->ID;
$page1->publishRecursive();
$page1->delete();
@ -210,18 +246,18 @@ class CMSMainTest extends FunctionalTest
$this->assertNull($cmsMain->getRecord('asdf'));
// Pages that are on draft and aren't on draft should both work
$this->assertInstanceOf(SiteTree::class, $cmsMain->getRecord($page1ID));
$this->assertInstanceOf(SiteTree::class, $cmsMain->getRecord($this->idFromFixture(SiteTree::class, 'page2')));
$this->assertInstanceOf('Page', $cmsMain->getRecord($page1ID));
$this->assertInstanceOf('Page', $cmsMain->getRecord($this->idFromFixture('Page', 'page2')));
// This functionality isn't actually used any more.
$newPage = $cmsMain->getRecord('new-Page-5');
$this->assertInstanceOf(SiteTree::class, $newPage);
$this->assertInstanceOf('Page', $newPage);
$this->assertEquals('5', $newPage->ParentID);
}
public function testDeletedPagesSiteTreeFilter()
{
$id = $this->idFromFixture(SiteTree::class, 'page3');
$id = $this->idFromFixture('Page', 'page3');
$this->logInWithPermission('ADMIN');
$result = $this->get('admin/pages/getsubtree?filter=CMSSiteTreeFilter_DeletedPages&ajax=1&ID=' . $id);
$this->assertEquals(200, $result->getStatusCode());
@ -242,7 +278,7 @@ class CMSMainTest extends FunctionalTest
'admin/pages/add/AddForm',
[
'ParentID' => '0',
'PageType' => RedirectorPage::class,
'PageType' => 'Page',
'Locale' => 'en_US',
'action_doAdd' => 1,
'ajax' => 1,
@ -262,7 +298,7 @@ class CMSMainTest extends FunctionalTest
'admin/pages/add/AddForm',
[
'ParentID' => '0',
'PageType' => RedirectorPage::class,
'PageType' => 'Page',
'Locale' => 'en_US',
'action_doAdd' => 1,
'ajax' => 1,
@ -296,7 +332,7 @@ class CMSMainTest extends FunctionalTest
'PageType' => CMSMainTest_ClassA::class,
'Locale' => 'en_US',
'action_doAdd' => 1,
'ajax' => 1,
'ajax' => 1
],
[
'X-Pjax' => 'CurrentForm,Breadcrumbs',
@ -316,7 +352,7 @@ class CMSMainTest extends FunctionalTest
'PageType' => CMSMainTest_ClassB::class,
'Locale' => 'en_US',
'action_doAdd' => 1,
'ajax' => 1,
'ajax' => 1
],
[
'X-Pjax' => 'CurrentForm,Breadcrumbs',
@ -339,10 +375,10 @@ class CMSMainTest extends FunctionalTest
'admin/pages/add/AddForm',
[
'ParentID' => $newPageId,
'PageType' => RedirectorPage::class,
'PageType' => 'Page',
'Locale' => 'en_US',
'action_doAdd' => 1,
'ajax' => 1,
'ajax' => 1
],
[
'X-Pjax' => 'CurrentForm,Breadcrumbs',
@ -357,8 +393,8 @@ class CMSMainTest extends FunctionalTest
public function testBreadcrumbs()
{
$page3 = $this->objFromFixture(SiteTree::class, 'page3');
$page31 = $this->objFromFixture(SiteTree::class, 'page31');
$page3 = $this->objFromFixture(Page::class, 'page3');
$page31 = $this->objFromFixture(Page::class, 'page31');
$this->logInAs('admin');
$response = $this->get('admin/pages/edit/show/' . $page31->ID);
@ -384,7 +420,7 @@ class CMSMainTest extends FunctionalTest
$this->assertEquals($page->Title, 'New Page');
$this->assertNotEquals($page->Sort, 0);
$this->assertInstanceOf(SiteTree::class, $page);
$this->assertInstanceOf('Page', $page);
// Test failure
try {
@ -413,10 +449,10 @@ class CMSMainTest extends FunctionalTest
);
// Change state of tree
$page1 = $this->objFromFixture(SiteTree::class, 'page1');
$page3 = $this->objFromFixture(SiteTree::class, 'page3');
$page11 = $this->objFromFixture(SiteTree::class, 'page11');
$page12 = $this->objFromFixture(SiteTree::class, 'page12');
$page1 = $this->objFromFixture(Page::class, 'page1');
$page3 = $this->objFromFixture(Page::class, 'page3');
$page11 = $this->objFromFixture(Page::class, 'page11');
$page12 = $this->objFromFixture(Page::class, 'page12');
// Deleted
$page1->doUnpublish();
$page1->delete();
@ -436,7 +472,7 @@ class CMSMainTest extends FunctionalTest
// Test deleted page filter
$params = [
'FilterClass' => 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_StatusDeletedPages',
'FilterClass' => 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_StatusDeletedPages'
];
$pages = $controller->getList($params);
$this->assertEquals(1, $pages->count());
@ -447,7 +483,7 @@ class CMSMainTest extends FunctionalTest
// Test live, but not on draft filter
$params = [
'FilterClass' => 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_StatusRemovedFromDraftPages',
'FilterClass' => 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_StatusRemovedFromDraftPages'
];
$pages = $controller->getList($params);
$this->assertEquals(1, $pages->count());
@ -458,7 +494,7 @@ class CMSMainTest extends FunctionalTest
// Test live pages filter
$params = [
'FilterClass' => 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_PublishedPages',
'FilterClass' => 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_PublishedPages'
];
$pages = $controller->getList($params);
$this->assertEquals(2, $pages->count());
@ -493,7 +529,7 @@ class CMSMainTest extends FunctionalTest
$this->loginWithPermission('ADMIN');
// Get a associated with a fixture page.
$page = $this->objFromFixture(SiteTree::class, 'page1');
$page = $this->objFromFixture(Page::class, 'page1');
$controller = CMSMain::create();
$controller->setRequest(Controller::curr()->getRequest());
$form = $controller->getEditForm($page->ID);
@ -521,7 +557,7 @@ class CMSMainTest extends FunctionalTest
$form->loadDataFrom(['ClassName' => CMSMainTest_ClassB::class]);
$result = $cms->save([
'ID' => $page->ID,
'ClassName' => CMSMainTest_ClassB::class,
'ClassName' => CMSMainTest_ClassB::class
], $form);
$this->assertEquals(200, $result->getStatusCode());

View File

@ -1,4 +1,4 @@
SilverStripe\CMS\Model\SiteTree:
Page:
page1:
Title: Page 1
Sort: 1
@ -10,11 +10,11 @@ SilverStripe\CMS\Model\SiteTree:
Sort: 3
page31:
Title: Page 3.1
Parent: =>SilverStripe\CMS\Model\SiteTree.page3
Parent: =>Page.page3
Sort: 1
page32:
Title: Page 3.2
Parent: =>SilverStripe\CMS\Model\SiteTree.page3
Parent: =>Page.page3
Sort: 2
page4:
Title: Page 4

View File

@ -2,11 +2,11 @@
namespace SilverStripe\CMS\Tests\Controllers;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\ValidationException;
use Page;
class CMSMainTest_ClassA extends SiteTree implements TestOnly
class CMSMainTest_ClassA extends Page implements TestOnly
{
private static $table_name = 'CMSMainTest_ClassA';

View File

@ -2,11 +2,11 @@
namespace SilverStripe\CMS\Tests\Controllers;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\ValidationException;
use Page;
class CMSMainTest_ClassB extends SiteTree implements TestOnly
class CMSMainTest_ClassB extends Page implements TestOnly
{
private static $table_name = 'CMSMainTest_ClassB';

View File

@ -2,10 +2,11 @@
namespace SilverStripe\CMS\Tests\Controllers;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\HiddenClass;
use Page;
class CMSMainTest_HiddenClass extends SiteTree implements TestOnly, HiddenClass
class CMSMainTest_HiddenClass extends Page implements TestOnly, HiddenClass
{
}

View File

@ -2,10 +2,10 @@
namespace SilverStripe\CMS\Tests\Controllers;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Dev\TestOnly;
use Page;
class CMSMainTest_NotRoot extends SiteTree implements TestOnly
class CMSMainTest_NotRoot extends Page implements TestOnly
{
private static $table_name = 'CMSMainTest_NotRoot';

View File

@ -0,0 +1,219 @@
<?php
namespace SilverStripe\CMS\Tests\Controllers;
use Page;
use SilverStripe\CMS\Controllers\CMSPageHistoryController;
use SilverStripe\CMS\Tests\Controllers\CMSPageHistoryControllerTest\HistoryController;
use SilverStripe\Control\Controller;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\FunctionalTest;
use SilverStripe\Forms\FieldGroup;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\HTMLReadonlyField;
use SilverStripe\Dev\Deprecation;
use SilverStripe\Forms\TextField;
class CMSPageHistoryControllerTest extends FunctionalTest
{
protected static $fixture_file = 'CMSPageHistoryControllerTest.yml';
protected $versionUnpublishedCheck;
protected $versionPublishCheck;
protected $versionUnpublishedCheck2;
protected $versionPublishCheck2;
protected $page;
protected static $extra_controllers = [
CMSPageHistoryControllerTest\HistoryController::class,
];
protected function setUp(): void
{
parent::setUp();
Deprecation::withNoReplacement(function () {
Injector::inst()->registerService(
new CMSPageHistoryController(),
CMSPageHistoryController::class
);
});
$this->loginWithPermission('ADMIN');
// creates a series of published, unpublished versions of a page
$this->page = new Page();
$this->page->URLSegment = "test";
$this->page->Content = "new content";
$this->page->write();
$this->versionUnpublishedCheck = $this->page->Version; // v1
$this->page->Content = "some further content";
$this->page->publishSingle();
$this->versionPublishCheck = $this->page->Version; // v2
$this->page->Content = "No, more changes please";
$this->page->Title = "Changing titles too";
$this->page->write();
$this->versionUnpublishedCheck2 = $this->page->Version; // v3
$this->page->Title = "Final Change";
$this->page->publishSingle();
$this->versionPublishCheck2 = $this->page->Version; // v4
}
public function testGetEditForm()
{
if (Deprecation::isEnabled()) {
$this->markTestSkipped('Test calls deprecated code');
}
$controller = new CMSPageHistoryController();
$controller->setRequest(Controller::curr()->getRequest());
// should get the latest version which we cannot rollback to
$form = $controller->getEditForm($this->page->ID);
$this->assertTrue($form->Actions()->dataFieldByName('action_doRollback')->isReadonly());
$this->assertEquals($this->page->ID, $form->Fields()->dataFieldByName('ID')->Value());
$this->assertEquals($this->versionPublishCheck2, $form->Fields()->dataFieldByName('Version')->Value());
$this->assertStringContainsString(
'Currently viewing the latest version',
$form->Fields()->fieldByName('Root.Main.CurrentlyViewingMessage')->getContent()
);
// edit form with a given version
$form = $controller->getEditForm($this->page->ID, null, $this->versionPublishCheck);
$this->assertFalse($form->Actions()->dataFieldByName('action_doRollback')->isReadonly());
$this->assertEquals($this->page->ID, $form->Fields()->dataFieldByName('ID')->Value());
$this->assertEquals($this->versionPublishCheck, $form->Fields()->dataFieldByName('Version')->Value());
$this->assertStringContainsString(
sprintf("Currently viewing version %s.", $this->versionPublishCheck),
$form->Fields()->fieldByName('Root.Main.CurrentlyViewingMessage')->getContent()
);
// check that compare mode updates the message
$form = $controller->getEditForm($this->page->ID, null, $this->versionPublishCheck, $this->versionPublishCheck2);
$this->assertStringContainsString(
sprintf("Comparing versions %s", $this->versionPublishCheck),
$form->Fields()->fieldByName('Root.Main.CurrentlyViewingMessage')->getContent()
);
$this->assertStringContainsString(
sprintf("and %s", $this->versionPublishCheck2),
$form->Fields()->fieldByName('Root.Main.CurrentlyViewingMessage')->getContent()
);
}
/**
* @todo should be less tied to cms theme.
* @todo check highlighting for comparing pages.
*/
public function testVersionsForm()
{
if (Deprecation::isEnabled()) {
$this->markTestSkipped('Test calls deprecated code');
}
$this->get('admin/pages/legacyhistory/show/' . $this->page->ID);
$form = $this->cssParser()->getBySelector('#Form_VersionsForm');
$this->assertEquals(1, count($form ?? []));
// check the page ID is present
$hidden = $form[0]->xpath("fieldset/input[@type='hidden']");
$this->assertThat($hidden, $this->logicalNot($this->isNull()), 'Hidden ID field exists');
$this->assertEquals($this->page->ID, (int) $hidden[0]->attributes()->value);
// ensure that all the versions are present in the table and displayed
$rows = $form[0]->xpath("fieldset/table/tbody/tr");
$this->assertEquals(4, count($rows ?? []));
}
public function testVersionsFormTableContainsInformation()
{
if (Deprecation::isEnabled()) {
$this->markTestSkipped('Test calls deprecated code');
}
$this->get('admin/pages/legacyhistory/show/' . $this->page->ID);
$form = $this->cssParser()->getBySelector('#Form_VersionsForm');
$rows = $form[0]->xpath("fieldset/table/tbody/tr");
$expected = [
['version' => $this->versionPublishCheck2, 'status' => 'published'],
['version' => $this->versionUnpublishedCheck2, 'status' => 'internal'],
['version' => $this->versionPublishCheck, 'status' => 'published'],
['version' => $this->versionUnpublishedCheck, 'status' => 'internal']
];
// goes the reverse order that we created in setUp()
$i = 0;
foreach ($rows as $tr) {
// data-link must be present for the javascript to load new
$this->assertStringContainsString($expected[$i]['status'], (string) $tr->attributes()->class);
$i++;
}
// test highlighting
$this->assertStringContainsString('active', (string) $rows[0]->attributes()->class);
$this->assertThat((string) $rows[1]->attributes()->class, $this->logicalNot($this->stringContains('active')));
}
public function testVersionsFormSelectsUnpublishedCheckbox()
{
if (Deprecation::isEnabled()) {
$this->markTestSkipped('Test calls deprecated code');
}
$this->get('admin/pages/legacyhistory/show/' . $this->page->ID);
$checkbox = $this->cssParser()->getBySelector('#Form_VersionsForm_ShowUnpublished');
$this->assertThat($checkbox[0], $this->logicalNot($this->isNull()));
$checked = $checkbox[0]->attributes()->checked ?: '';
$this->assertThat($checked, $this->logicalNot($this->stringContains('checked')));
// viewing an unpublished
$this->get('admin/pages/legacyhistory/show/' . $this->page->ID . '/' . $this->versionUnpublishedCheck);
$checkbox = $this->cssParser()->getBySelector('#Form_VersionsForm_ShowUnpublished');
$this->assertThat($checkbox[0], $this->logicalNot($this->isNull()));
$this->assertEquals('checked', (string) $checkbox[0]->attributes()->checked);
}
public function testTransformReadonly()
{
if (Deprecation::isEnabled()) {
$this->markTestSkipped('Test calls deprecated code');
}
/** @var CMSPageHistoryController $history */
$history = new CMSPageHistoryController();
$history->setRequest(Controller::curr()->getRequest());
$fieldList = FieldList::create([
FieldGroup::create('group', [
TextField::create('childField', 'child field'),
]),
TextField::create('field', 'field', 'My <del>value</del><ins>change</ins>'),
HiddenField::create('hiddenField', 'hidden field'),
]);
$newList = $history->transformReadonly($fieldList);
$field = $newList->dataFieldByName('field');
$this->assertTrue($field instanceof HTMLReadonlyField);
$this->assertStringContainsString('<ins>', $field->forTemplate());
$groupField = $newList->fieldByName('group');
$this->assertTrue($groupField instanceof FieldGroup);
$childField = $newList->dataFieldByName('childField');
$this->assertTrue($childField instanceof HTMLReadonlyField);
$hiddenField = $newList->dataFieldByName('hiddenField');
$this->assertTrue($hiddenField instanceof HiddenField);
}
}

View File

@ -0,0 +1,10 @@
Page:
page1:
Title: Page 1
Sort: 1
page2:
Title: Page 2
Sort: 2
page3:
Title: Page 3
Sort: 3

View File

@ -0,0 +1,15 @@
<?php
namespace SilverStripe\CMS\Tests\Controllers\CMSPageHistoryControllerTest;
use SilverStripe\CMS\Controllers\CMSPageHistoryController;
use SilverStripe\Dev\TestOnly;
/**
* Used to circumvent potential URL conflicts with the silverstripe/versioned-admin history viewer controller
* when running unit tests on the legacy CMSPageHistoryController.
*/
class HistoryController extends CMSPageHistoryController implements TestOnly
{
private static $url_segment = 'pages/legacyhistory';
}

View File

@ -2,6 +2,7 @@
namespace SilverStripe\CMS\Tests\Controllers;
use Page;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Versioned\Versioned;
use SilverStripe\CMS\Controllers\CMSSiteTreeFilter_Search;
@ -14,12 +15,13 @@ use SilverStripe\Dev\SapphireTest;
class CMSSiteTreeFilterTest extends SapphireTest
{
protected static $fixture_file = 'CMSSiteTreeFilterTest.yml';
public function testSearchFilterEmpty()
{
$page1 = $this->objFromFixture(SiteTree::class, 'page1');
$page2 = $this->objFromFixture(SiteTree::class, 'page2');
$page1 = $this->objFromFixture('Page', 'page1');
$page2 = $this->objFromFixture('Page', 'page2');
$f = new CMSSiteTreeFilter_Search();
$results = $f->pagesIncluded();
@ -30,8 +32,8 @@ class CMSSiteTreeFilterTest extends SapphireTest
public function testSearchFilterByTitle()
{
$page1 = $this->objFromFixture(SiteTree::class, 'page1');
$page2 = $this->objFromFixture(SiteTree::class, 'page2');
$page1 = $this->objFromFixture('Page', 'page1');
$page2 = $this->objFromFixture('Page', 'page2');
$f = new CMSSiteTreeFilter_Search(['Title' => 'Page 1']);
$results = $f->pagesIncluded();
@ -47,7 +49,7 @@ class CMSSiteTreeFilterTest extends SapphireTest
public function testUrlSegmentFilter()
{
$page = $this->objFromFixture(SiteTree::class, 'page8');
$page = $this->objFromFixture(Page::class, 'page8');
$filter = CMSSiteTreeFilter_Search::create(['Term' => 'lake-wanaka+adventure']);
$this->assertTrue($filter->isPageIncluded($page));
@ -58,8 +60,8 @@ class CMSSiteTreeFilterTest extends SapphireTest
public function testIncludesParentsForNestedMatches()
{
$parent = $this->objFromFixture(SiteTree::class, 'page3');
$child = $this->objFromFixture(SiteTree::class, 'page3b');
$parent = $this->objFromFixture('Page', 'page3');
$child = $this->objFromFixture('Page', 'page3b');
$f = new CMSSiteTreeFilter_Search(['Title' => 'Page 3b']);
$results = $f->pagesIncluded();
@ -76,11 +78,11 @@ class CMSSiteTreeFilterTest extends SapphireTest
public function testChangedPagesFilter()
{
/** @var Page $unchangedPage */
$unchangedPage = $this->objFromFixture(SiteTree::class, 'page1');
$unchangedPage = $this->objFromFixture('Page', 'page1');
$unchangedPage->publishRecursive();
/** @var Page $changedPage */
$changedPage = $this->objFromFixture(SiteTree::class, 'page2');
$changedPage = $this->objFromFixture('Page', 'page2');
$changedPage->Title = 'Original';
$changedPage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$changedPage->Title = 'Changed';
@ -119,7 +121,7 @@ class CMSSiteTreeFilterTest extends SapphireTest
public function testDeletedPagesFilter()
{
$deletedPage = $this->objFromFixture(SiteTree::class, 'page2');
$deletedPage = $this->objFromFixture('Page', 'page2');
$deletedPage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$deletedPageID = $deletedPage->ID;
$deletedPage->delete();
@ -139,7 +141,7 @@ class CMSSiteTreeFilterTest extends SapphireTest
public function testStatusDraftPagesFilter()
{
$draftPage = $this->objFromFixture(SiteTree::class, 'page4');
$draftPage = $this->objFromFixture('Page', 'page4');
$draftPage = Versioned::get_one_by_stage(
SiteTree::class,
'Stage',
@ -162,23 +164,20 @@ class CMSSiteTreeFilterTest extends SapphireTest
public function testDateFromToLastSameDate()
{
$draftPage = $this->objFromFixture(SiteTree::class, 'page4');
$draftPage = $this->objFromFixture('Page', 'page4');
// Grab the date
$date = substr($draftPage->LastEdited ?? '', 0, 10);
// Filter with that date
$filter = new CMSSiteTreeFilter_Search([
'LastEditedFrom' => $date,
'LastEditedTo' => $date,
'LastEditedTo' => $date
]);
$this->assertTrue(
$filter->isPageIncluded($draftPage),
'Using the same date for from and to should show find that page'
);
$this->assertTrue($filter->isPageIncluded($draftPage), 'Using the same date for from and to should show find that page');
}
public function testStatusRemovedFromDraftFilter()
{
$removedDraftPage = $this->objFromFixture(SiteTree::class, 'page6');
$removedDraftPage = $this->objFromFixture('Page', 'page6');
$removedDraftPage->publishRecursive();
$removedDraftPage->deleteFromStage('Stage');
$removedDraftPage = Versioned::get_one_by_stage(
@ -203,7 +202,7 @@ class CMSSiteTreeFilterTest extends SapphireTest
public function testStatusDeletedFilter()
{
$deletedPage = $this->objFromFixture(SiteTree::class, 'page7');
$deletedPage = $this->objFromFixture('Page', 'page7');
$deletedPage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$deletedPageID = $deletedPage->ID;

View File

@ -1,4 +1,4 @@
SilverStripe\CMS\Model\SiteTree:
Page:
page1:
Title: Page 1
page2:
@ -15,19 +15,19 @@ SilverStripe\CMS\Model\SiteTree:
page7:
Title: Page 7
page7a:
Parent: =>SilverStripe\CMS\Model\SiteTree.page7
Parent: =>Page.page7
Title: Page 7a
page2a:
Parent: =>SilverStripe\CMS\Model\SiteTree.page2
Parent: =>Page.page2
Title: Page 2a
page2b:
Parent: =>SilverStripe\CMS\Model\SiteTree.page2
Parent: =>Page.page2
Title: Page 2b
page3a:
Parent: =>SilverStripe\CMS\Model\SiteTree.page3
Parent: =>Page.page3
Title: Page 3a
page3b:
Parent: =>SilverStripe\CMS\Model\SiteTree.page3
Parent: =>Page.page3
Title: Page 3b
page8:
Title: EncodedUrlSegment

View File

@ -32,7 +32,7 @@ class CMSTreeTest extends FunctionalTest
$data = [
'SiblingIDs' => $siblingIDs,
'ID' => $page2->ID,
'ParentID' => 0,
'ParentID' => 0
];
$response = $this->post('admin/pages/edit/savetreenode', $data);
@ -62,12 +62,12 @@ class CMSTreeTest extends FunctionalTest
$siblingIDs = [
$page31->ID,
$page2->ID,
$page32->ID,
$page32->ID
];
$data = [
'SiblingIDs' => $siblingIDs,
'ID' => $page2->ID,
'ParentID' => $page3->ID,
'ParentID' => $page3->ID
];
$response = $this->post('admin/pages/edit/savetreenode', $data);
$this->assertEquals(200, $response->getStatusCode());
@ -95,7 +95,7 @@ class CMSTreeTest extends FunctionalTest
$this->logInWithPermission('ADMIN');
// Check page
$result = $this->get('admin/pages/edit/updatetreenodes?ids=' . $page1->ID);
$result = $this->get('admin/pages/edit/updatetreenodes?ids='.$page1->ID);
$this->assertEquals(200, $result->getStatusCode());
$this->assertEquals('application/json', $result->getHeader('Content-Type'));
$data = json_decode($result->getBody() ?? '', true);
@ -105,7 +105,7 @@ class CMSTreeTest extends FunctionalTest
$this->assertEmpty($pageData['PrevID']);
// check subpage
$result = $this->get('admin/pages/edit/updatetreenodes?ids=' . $page31->ID);
$result = $this->get('admin/pages/edit/updatetreenodes?ids='.$page31->ID);
$this->assertEquals(200, $result->getStatusCode());
$this->assertEquals('application/json', $result->getHeader('Content-Type'));
$data = json_decode($result->getBody() ?? '', true);
@ -115,7 +115,7 @@ class CMSTreeTest extends FunctionalTest
$this->assertEmpty($pageData['PrevID']);
// Multiple pages
$result = $this->get('admin/pages/edit/updatetreenodes?ids=' . $page1->ID . ',' . $page2->ID);
$result = $this->get('admin/pages/edit/updatetreenodes?ids='.$page1->ID.','.$page2->ID);
$this->assertEquals(200, $result->getStatusCode());
$this->assertEquals('application/json', $result->getHeader('Content-Type'));
$data = json_decode($result->getBody() ?? '', true);

View File

@ -6,7 +6,7 @@ use SilverStripe\Versioned\Versioned;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\FunctionalTest;
use SilverStripe\CMS\Model\SiteTree;
use Page;
class ContentControllerPermissionsTest extends FunctionalTest
{
@ -17,7 +17,7 @@ class ContentControllerPermissionsTest extends FunctionalTest
public function testCanViewStage()
{
// Create a new page
$page = new SiteTree();
$page = new Page();
$page->URLSegment = 'testpage';
$page->write();
$page->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
@ -38,11 +38,7 @@ class ContentControllerPermissionsTest extends FunctionalTest
$response = $responseException->getResponse();
}
// should redirect to login
$this->assertEquals(
$response->getStatusCode(),
302,
'Redirects to login page when not logged in for draft stage'
);
$this->assertEquals($response->getStatusCode(), 302, 'Redirects to login page when not logged in for draft stage');
$this->assertStringContainsString(
Config::inst()->get('SilverStripe\\Security\\Security', 'login_url'),
$response->getHeader('Location')
@ -51,10 +47,6 @@ class ContentControllerPermissionsTest extends FunctionalTest
$this->logInWithPermission('CMS_ACCESS_CMSMain');
$response = $this->get('/testpage/?stage=Stage');
$this->assertEquals(
$response->getStatusCode(),
200,
'Doesnt redirect to login, but shows page for authenticated user'
);
$this->assertEquals($response->getStatusCode(), 200, 'Doesnt redirect to login, but shows page for authenticated user');
}
}

View File

@ -4,23 +4,23 @@ namespace SilverStripe\CMS\Tests\Controllers;
use SilverStripe\Assets\File;
use SilverStripe\CMS\Controllers\ContentController;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\CMS\Search\ContentControllerSearchExtension;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\Search\FulltextSearchable;
use SilverStripe\Versioned\Versioned;
use Page;
class ContentControllerSearchExtensionTest extends SapphireTest
{
protected static $required_extensions = [
ContentController::class => [
ContentControllerSearchExtension::class,
],
]
];
public function testCustomSearchFormClassesToTest()
{
$page = new SiteTree();
$page = new Page();
$page->URLSegment = 'whatever';
$page->Content = 'oh really?';
$page->write();

10
tests/php/Controllers/ContentControllerTest.php Normal file → Executable file
View File

@ -10,6 +10,7 @@ use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\FunctionalTest;
use SilverStripe\Versioned\Versioned;
use Page;
class ContentControllerTest extends FunctionalTest
{
@ -30,8 +31,8 @@ class ContentControllerTest extends FunctionalTest
Config::modify()->set(SiteTree::class, 'nested_urls', true);
// Ensure all pages are published
/** @var SiteTree $page */
foreach (SiteTree::get() as $page) {
/** @var Page $page */
foreach (Page::get() as $page) {
$page->publishSingle();
}
}
@ -94,7 +95,8 @@ class ContentControllerTest extends FunctionalTest
public function testDeepNestedURLs()
{
$page = new SiteTree();
$page = new Page();
$page->URLSegment = 'base-page';
$page->write();
$page->publishSingle();
@ -172,7 +174,7 @@ class ContentControllerTest extends FunctionalTest
$response = $this->get($page->RelativeLink());
$this->assertEquals("ContentControllerTestPageWithoutController", trim($response->getBody() ?? ''));
// This should fall over to user Page.ss
// // This should fall over to user Page.ss
$page = new ContentControllerTestPage();
$page->URLSegment = "test";
$page->write();

View File

@ -2,10 +2,10 @@
namespace SilverStripe\CMS\Tests\Controllers;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Dev\TestOnly;
use Page;
class ContentControllerTestPage extends SiteTree implements TestOnly
class ContentControllerTestPage extends Page implements TestOnly
{
private static $table_name = 'ContentControllerTestPage';
}

View File

@ -9,7 +9,7 @@ class ContentControllerTestPageController extends PageController implements Test
{
private static $allowed_actions = [
'test',
'testwithouttemplate',
'testwithouttemplate'
];
public function testwithouttemplate()

View File

@ -2,9 +2,10 @@
namespace SilverStripe\CMS\Tests\Controllers;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Dev\TestOnly;
use Page;
class ContentControllerTestPageWithoutController extends SiteTree implements TestOnly
class ContentControllerTestPageWithoutController extends Page implements TestOnly
{
}

View File

@ -2,10 +2,10 @@
namespace SilverStripe\CMS\Tests\Controllers;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Dev\TestOnly;
use Page;
class ContentControllerTest_Page extends SiteTree implements TestOnly
class ContentControllerTest_Page extends Page implements TestOnly
{
private static $table_name = 'ContentControllerTest_Page';
}

View File

@ -2,13 +2,14 @@
namespace SilverStripe\CMS\Tests\Controllers;
use SilverStripe\CMS\Controllers\ContentController;
use SilverStripe\Dev\TestOnly;
use PageController;
class ContentControllerTest_PageController extends ContentController implements TestOnly
class ContentControllerTest_PageController extends PageController implements TestOnly
{
private static $allowed_actions = [
'second_index',
'second_index'
];
public function index()

View File

@ -10,6 +10,7 @@ use SilverStripe\Core\Config\Config;
use SilverStripe\Control\Director;
use SilverStripe\Control\Controller;
use SilverStripe\Dev\FunctionalTest;
use Page;
use SilverStripe\View\Parsers\URLSegmentFilter;
class ModelAsControllerTest extends FunctionalTest
@ -33,7 +34,7 @@ class ModelAsControllerTest extends FunctionalTest
protected function generateNestedPagesFixture()
{
$level1 = new SiteTree();
$level1 = new Page();
$level1->Title = 'First Level';
$level1->URLSegment = 'level1';
$level1->write();
@ -43,7 +44,7 @@ class ModelAsControllerTest extends FunctionalTest
$level1->write();
$level1->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$level2 = new SiteTree();
$level2 = new Page();
$level2->Title = 'Second Level';
$level2->URLSegment = 'level2';
$level2->ParentID = $level1->ID;
@ -54,7 +55,7 @@ class ModelAsControllerTest extends FunctionalTest
$level2->write();
$level2->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$level3 = new SiteTree();
$level3 = new Page();
$level3->Title = "Level 3";
$level3->URLSegment = 'level3';
$level3->ParentID = $level2->ID;
@ -92,7 +93,7 @@ class ModelAsControllerTest extends FunctionalTest
$response = $this->get('newlevel1/level2');
$this->assertEquals($response->getStatusCode(), 301);
$this->assertEquals(
Controller::join_links(Director::baseURL() . 'newlevel1/newlevel2'),
Controller::join_links(Director::baseURL() . 'newlevel1/newlevel2/'),
$response->getHeader('Location')
);
@ -100,7 +101,7 @@ class ModelAsControllerTest extends FunctionalTest
$response = $this->get('newlevel1/newlevel2/level3');
$this->assertEquals($response->getStatusCode(), 301);
$this->assertEquals(
Controller::join_links(Director::baseURL() . 'newlevel1/newlevel2/newlevel3'),
Controller::join_links(Director::baseURL() . 'newlevel1/newlevel2/newlevel3/'),
$response->getHeader('Location')
);
@ -114,7 +115,7 @@ class ModelAsControllerTest extends FunctionalTest
*/
public function testHeavilyNestedRenamedRedirectedPages()
{
$page = new SiteTree();
$page = new Page();
$page->Title = 'First Level';
$page->URLSegment = 'oldurl';
$page->write();
@ -124,28 +125,28 @@ class ModelAsControllerTest extends FunctionalTest
$page->write();
$page->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$page2 = new SiteTree();
$page2 = new Page();
$page2->Title = 'Second Level Page';
$page2->URLSegment = 'level2';
$page2->ParentID = $page->ID;
$page2->write();
$page2->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$page3 = new SiteTree();
$page3 = new Page();
$page3->Title = 'Third Level Page';
$page3->URLSegment = 'level3';
$page3->ParentID = $page2->ID;
$page3->write();
$page3->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$page4 = new SiteTree();
$page4 = new Page();
$page4->Title = 'Fourth Level Page';
$page4->URLSegment = 'level4';
$page4->ParentID = $page3->ID;
$page4->write();
$page4->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$page5 = new SiteTree();
$page5 = new Page();
$page5->Title = 'Fifth Level Page';
$page5->URLSegment = 'level5';
$page5->ParentID = $page4->ID;
@ -153,10 +154,10 @@ class ModelAsControllerTest extends FunctionalTest
$page5->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
// Test that the redirect still works fine when trying to access the most nested page
$response = $this->get('oldurl/level2/level3/level4/level5');
$response = $this->get('oldurl/level2/level3/level4/level5/');
$this->assertEquals($response->getStatusCode(), 301);
$this->assertEquals(
Controller::join_links(Director::baseURL() . 'newurl/level2/level3/level4/level5'),
Controller::join_links(Director::baseURL() . 'newurl/level2/level3/level4/level5/'),
$response->getHeader('Location')
);
}
@ -170,7 +171,7 @@ class ModelAsControllerTest extends FunctionalTest
$response = $this->get('newlevel3');
$this->assertEquals(301, $response->getStatusCode());
$this->assertEquals(
Director::baseURL() . 'newlevel1/newlevel2/newlevel3',
Director::baseURL() . 'newlevel1/newlevel2/newlevel3/',
$response->getHeader("Location")
);
@ -178,7 +179,7 @@ class ModelAsControllerTest extends FunctionalTest
$response = $this->get('level3');
$this->assertEquals(301, $response->getStatusCode());
$this->assertEquals(
Director::baseURL() . 'newlevel1/newlevel2/newlevel3',
Director::baseURL() . 'newlevel1/newlevel2/newlevel3/',
$response->getHeader("Location")
);
}
@ -187,8 +188,8 @@ class ModelAsControllerTest extends FunctionalTest
{
$this->generateNestedPagesFixture();
$otherParent = new SiteTree([
'URLSegment' => 'otherparent',
$otherParent = new Page([
'URLSegment' => 'otherparent'
]);
$otherParent->write();
$otherParent->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
@ -214,10 +215,10 @@ class ModelAsControllerTest extends FunctionalTest
$this->generateNestedPagesFixture();
// check third level URLSegment
$response = $this->get('newlevel1/newlevel2/level3?foo=bar&test=test');
$response = $this->get('newlevel1/newlevel2/level3/?foo=bar&test=test');
$this->assertEquals($response->getStatusCode(), 301);
$this->assertEquals(
Controller::join_links(Director::baseURL() . 'newlevel1/newlevel2/newlevel3', '?foo=bar&test=test'),
Controller::join_links(Director::baseURL() . 'newlevel1/newlevel2/newlevel3/', '?foo=bar&test=test'),
$response->getHeader('Location')
);
}
@ -231,9 +232,9 @@ class ModelAsControllerTest extends FunctionalTest
{
$this->generateNestedPagesFixture();
$otherLevel1 = new SiteTree([
$otherLevel1 = new Page([
'Title' => "Other Level 1",
'URLSegment' => 'level1',
'URLSegment' => 'level1'
]);
$otherLevel1->write();
$otherLevel1->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
@ -259,7 +260,7 @@ class ModelAsControllerTest extends FunctionalTest
*/
public function testFindOldPage()
{
$page = new SiteTree();
$page = new Page();
$page->Title = 'First Level';
$page->URLSegment = 'oldurl';
$page->write();
@ -273,7 +274,7 @@ class ModelAsControllerTest extends FunctionalTest
$matchedPage = SiteTree::get_by_link($url);
$this->assertEquals('First Level', $matchedPage->Title);
$page2 = new SiteTree();
$page2 = new Page();
$page2->Title = 'Second Level Page';
$page2->URLSegment = 'oldpage2';
$page2->ParentID = $page->ID;
@ -303,12 +304,12 @@ class ModelAsControllerTest extends FunctionalTest
RootURLController::reset();
Config::modify()->set(SiteTree::class, 'nested_urls', true);
$draft = new SiteTree();
$draft = new Page();
$draft->Title = 'Root Leve Draft Page';
$draft->URLSegment = 'root';
$draft->write();
$published = new SiteTree();
$published = new Page();
$published->Title = 'Published Page Under Draft Page';
$published->URLSegment = 'sub-root';
$published->write();
@ -318,8 +319,7 @@ class ModelAsControllerTest extends FunctionalTest
$this->assertEquals(
$response->getStatusCode(),
404,
'The page should not be found since its parent has not been published, in this case ' .
'http://<yousitename>/root/sub-root or http://<yousitename>/sub-root'
'The page should not be found since its parent has not been published, in this case http://<yousitename>/root/sub-root or http://<yousitename>/sub-root'
);
}
@ -327,13 +327,13 @@ class ModelAsControllerTest extends FunctionalTest
{
Config::modify()->set(URLSegmentFilter::class, 'default_allow_multibyte', true);
$parent = new SiteTree();
$parent = new Page();
$parent->Title = 'Multibyte test';
$parent->URLSegment = 'بلاگ';
$parent->write();
$parent->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$child = new SiteTree();
$child = new Page();
$child->Title = 'Multibyte test';
$child->URLSegment = 'فضة';
$child->ParentID = $parent->ID;

View File

@ -13,7 +13,7 @@ class RootURLControllerTest extends SapphireTest
public function testGetHomepageLink()
{
$default = $this->objFromFixture(SiteTree::class, 'home');
$default = $this->objFromFixture('Page', 'home');
Config::modify()->set(SiteTree::class, 'nested_urls', false);
$this->assertEquals('home', RootURLController::get_homepage_link());

View File

@ -1,9 +1,9 @@
SilverStripe\CMS\Model\SiteTree:
Page:
home:
Title: Home
nested:
Title: Nested Home
Parent: =>SilverStripe\CMS\Model\SiteTree.home
Parent: =>Page.home
page1:
Title: First Page
URLSegment: page1

View File

@ -0,0 +1,239 @@
<?php
namespace SilverStripe\CMS\Tests\Controllers;
use SilverStripe\CMS\Controllers\SilverStripeNavigator;
use SilverStripe\CMS\Controllers\SilverStripeNavigatorItem_ArchiveLink;
use SilverStripe\CMS\Controllers\SilverStripeNavigatorItem_LiveLink;
use SilverStripe\CMS\Controllers\SilverStripeNavigatorItem_StageLink;
use SilverStripe\CMS\Controllers\SilverStripeNavigatorItem_Unversioned;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Security\Member;
class SilverStripeNavigatorTest extends SapphireTest
{
protected static $fixture_file = 'CMSMainTest.yml';
protected static $extra_dataobjects = [
SilverStripeNavigatorTest\UnstagedRecord::class,
SilverStripeNavigatorTest\UnversionedRecord::class,
SilverStripeNavigatorTest\VersionedRecord::class,
];
public function testGetItemsAutoDiscovery(): void
{
$record = new SilverStripeNavigatorTest\VersionedRecord();
$record->PreviewLinkTestProperty = 'some-value';
$record->write();
$navigator = new SilverStripeNavigator($record);
$classes = array_map('get_class', $navigator->getItems()->toArray());
$this->assertContains(
SilverStripeNavigatorTest_TestItem::class,
$classes,
'Autodiscovers new classes'
);
}
public function testGetItemsPublished(): void
{
$record = new SilverStripeNavigatorTest\VersionedRecord();
$record->PreviewLinkTestProperty = 'some-value';
$record->write();
$record->publishRecursive();
$navigator = new SilverStripeNavigator($record);
$classes = array_map('get_class', $navigator->getItems()->toArray());
// Has the live and staged links
$this->assertContains(SilverStripeNavigatorItem_LiveLink::class, $classes);
$this->assertContains(SilverStripeNavigatorItem_StageLink::class, $classes);
// Does not have the other links
$this->assertNotContains(SilverStripeNavigatorItem_ArchiveLink::class, $classes);
$this->assertNotContains(SilverStripeNavigatorItem_Unversioned::class, $classes);
}
public function testGetItemsStaged(): void
{
$record = new SilverStripeNavigatorTest\VersionedRecord();
$record->PreviewLinkTestProperty = 'some-value';
$record->write();
$navigator = new SilverStripeNavigator($record);
$classes = array_map('get_class', $navigator->getItems()->toArray());
// Has the stage link
$this->assertContains(SilverStripeNavigatorItem_StageLink::class, $classes);
// Does not have the other links
$this->assertNotContains(SilverStripeNavigatorItem_ArchiveLink::class, $classes);
$this->assertNotContains(SilverStripeNavigatorItem_LiveLink::class, $classes);
$this->assertNotContains(SilverStripeNavigatorItem_Unversioned::class, $classes);
}
public function testGetItemsArchived(): void
{
$record = new SilverStripeNavigatorTest\VersionedRecord();
$record->PreviewLinkTestProperty = 'some-value';
$record->write();
$record->doArchive();
$navigator = new SilverStripeNavigator($record);
$classes = array_map('get_class', $navigator->getItems()->toArray());
// Has the archived link
$this->assertContains(SilverStripeNavigatorItem_ArchiveLink::class, $classes);
// Does not have the other links
$this->assertNotContains(SilverStripeNavigatorItem_LiveLink::class, $classes);
$this->assertNotContains(SilverStripeNavigatorItem_StageLink::class, $classes);
$this->assertNotContains(SilverStripeNavigatorItem_UnversionedLink::class, $classes);
}
public function testGetItemsUnstaged(): void
{
$record = new SilverStripeNavigatorTest\UnstagedRecord();
$record->previewLinkTestProperty = 'some-value';
$record->write();
$navigator = new SilverStripeNavigator($record);
$classes = array_map('get_class', $navigator->getItems()->toArray());
// Has the unversioned link
$this->assertContains(SilverStripeNavigatorItem_Unversioned::class, $classes);
// Does not have the other links
$this->assertNotContains(SilverStripeNavigatorItem_ArchiveLink::class, $classes);
$this->assertNotContains(SilverStripeNavigatorItem_LiveLink::class, $classes);
$this->assertNotContains(SilverStripeNavigatorItem_StageLink::class, $classes);
}
public function testGetItemsUnversioned(): void
{
$record = new SilverStripeNavigatorTest\UnversionedRecord();
$record->previewLinkTestProperty = 'some-value';
$record->write();
$navigator = new SilverStripeNavigator($record);
$classes = array_map('get_class', $navigator->getItems()->toArray());
// Has the unversioned link
$this->assertContains(SilverStripeNavigatorItem_Unversioned::class, $classes);
// Does not have the other links
$this->assertNotContains(SilverStripeNavigatorItem_ArchiveLink::class, $classes);
$this->assertNotContains(SilverStripeNavigatorItem_LiveLink::class, $classes);
$this->assertNotContains(SilverStripeNavigatorItem_StageLink::class, $classes);
}
public function testCanViewPublished(): void
{
$record = new SilverStripeNavigatorTest\VersionedRecord();
$record->write();
$record->publishRecursive();
$liveLinkItem = new SilverStripeNavigatorItem_LiveLink($record);
$stagedLinkItem = new SilverStripeNavigatorItem_StageLink($record);
$archivedLinkItem = new SilverStripeNavigatorItem_ArchiveLink($record);
$unversionedLinkItem = new SilverStripeNavigatorItem_Unversioned($record);
// Cannot view staged and live links when there's no preview link
$this->assertFalse($liveLinkItem->canView());
$this->assertFalse($stagedLinkItem->canView());
$record->PreviewLinkTestProperty = 'some-value';
$record->write();
$record->publishRecursive();
// Can view staged and live links
$this->assertTrue($liveLinkItem->canView());
$this->assertTrue($stagedLinkItem->canView());
// Cannot view the other links
$this->assertFalse($archivedLinkItem->canView());
$this->assertFalse($unversionedLinkItem->canView());
}
public function testCanViewStaged(): void
{
$record = new SilverStripeNavigatorTest\VersionedRecord();
$record->write();
$liveLinkItem = new SilverStripeNavigatorItem_LiveLink($record);
$stagedLinkItem = new SilverStripeNavigatorItem_StageLink($record);
$archivedLinkItem = new SilverStripeNavigatorItem_ArchiveLink($record);
$unversionedLinkItem = new SilverStripeNavigatorItem_Unversioned($record);
// Cannot view staged link when there's no preview link
$this->assertFalse($stagedLinkItem->canView());
$record->PreviewLinkTestProperty = 'some-value';
// Can view staged link
$this->assertTrue($stagedLinkItem->canView());
// Cannot view the other links
$this->assertFalse($liveLinkItem->canView());
$this->assertFalse($archivedLinkItem->canView());
$this->assertFalse($unversionedLinkItem->canView());
}
public function testCanViewArchived(): void
{
$record = new SilverStripeNavigatorTest\VersionedRecord();
$record->write();
$record->doArchive();
$liveLinkItem = new SilverStripeNavigatorItem_LiveLink($record);
$stagedLinkItem = new SilverStripeNavigatorItem_StageLink($record);
$archivedLinkItem = new SilverStripeNavigatorItem_ArchiveLink($record);
$unversionedLinkItem = new SilverStripeNavigatorItem_Unversioned($record);
// Cannot view archived link when there's no preview link
$this->assertFalse($archivedLinkItem->canView());
$record->PreviewLinkTestProperty = 'some-value';
// Can view archived link
$this->assertTrue($archivedLinkItem->canView());
// Cannot view the other links
$this->assertFalse($liveLinkItem->canView());
$this->assertFalse($stagedLinkItem->canView());
$this->assertFalse($unversionedLinkItem->canView());
}
public function testCanViewUnstaged(): void
{
$record = new SilverStripeNavigatorTest\UnstagedRecord();
$record->write();
$liveLinkItem = new SilverStripeNavigatorItem_LiveLink($record);
$stagedLinkItem = new SilverStripeNavigatorItem_StageLink($record);
$archivedLinkItem = new SilverStripeNavigatorItem_ArchiveLink($record);
$unversionedLinkItem = new SilverStripeNavigatorItem_Unversioned($record);
// Cannot view unversioned link when there's no preview link
$this->assertFalse($unversionedLinkItem->canView());
$record->previewLinkTestProperty = 'some-value';
// Can view unversioned link
$this->assertTrue($unversionedLinkItem->canView());
// Cannot view the other links
$this->assertFalse($liveLinkItem->canView());
$this->assertFalse($stagedLinkItem->canView());
$this->assertFalse($archivedLinkItem->canView());
}
public function testCanViewUnversioned(): void
{
$record = new SilverStripeNavigatorTest\UnversionedRecord();
$record->write();
$liveLinkItem = new SilverStripeNavigatorItem_LiveLink($record);
$stagedLinkItem = new SilverStripeNavigatorItem_StageLink($record);
$archivedLinkItem = new SilverStripeNavigatorItem_ArchiveLink($record);
$unversionedLinkItem = new SilverStripeNavigatorItem_Unversioned($record);
// Cannot view unversioned link when there's no preview link
$this->assertFalse($unversionedLinkItem->canView());
$record->previewLinkTestProperty = 'some-value';
// Can view unversioned link
$this->assertTrue($unversionedLinkItem->canView());
// Cannot view the other links
$this->assertFalse($liveLinkItem->canView());
$this->assertFalse($stagedLinkItem->canView());
$this->assertFalse($archivedLinkItem->canView());
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace SilverStripe\CMS\Tests\Controllers\SilverStripeNavigatorTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\CMSPreviewable;
use SilverStripe\ORM\DataObject;
use SilverStripe\Versioned\Versioned;
/**
* Versioned but not staged
*/
class UnstagedRecord extends DataObject implements TestOnly, CMSPreviewable
{
private static $table_name = 'SilverStripeNavigatorTest_UnstagedRecord';
private static $show_stage_link = true;
private static $show_live_link = true;
private static $show_unversioned_preview_link = true;
private static $extensions = [
Versioned::class . '.versioned',
];
public $previewLinkTestProperty = null;
public function PreviewLink($action = null)
{
return $this->previewLinkTestProperty;
}
/**
* To determine preview mechanism (e.g. embedded / iframe)
*
* @return string
*/
public function getMimeType()
{
return 'text/html';
}
public function CMSEditLink()
{
return null;
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace SilverStripe\CMS\Tests\Controllers\SilverStripeNavigatorTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\CMSPreviewable;
use SilverStripe\ORM\DataObject;
use SilverStripe\Versioned\Versioned;
class UnversionedRecord extends DataObject implements TestOnly, CMSPreviewable
{
private static $table_name = 'SilverStripeNavigatorTest_UnversionedRecord';
private static $show_stage_link = true;
private static $show_live_link = true;
private static $show_unversioned_preview_link = true;
public $previewLinkTestProperty = null;
public function PreviewLink($action = null)
{
return $this->previewLinkTestProperty;
}
public function getMimeType()
{
return 'text/html';
}
public function CMSEditLink()
{
return null;
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace SilverStripe\CMS\Tests\Controllers\SilverStripeNavigatorTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\CMSPreviewable;
use SilverStripe\ORM\DataObject;
use SilverStripe\Versioned\Versioned;
class VersionedRecord extends DataObject implements TestOnly, CMSPreviewable
{
private static $table_name = 'SilverStripeNavigatorTest_VersionedRecord';
private static $show_stage_link = true;
private static $show_live_link = true;
private static $show_unversioned_preview_link = true;
private static $db = [
'PreviewLinkTestProperty' => 'Text',
];
private static $extensions = [
Versioned::class,
];
public function PreviewLink($action = null)
{
return $this->PreviewLinkTestProperty;
}
public function getMimeType()
{
return 'text/html';
}
public function CMSEditLink()
{
return null;
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace SilverStripe\CMS\Tests\Controllers;
use SilverStripe\CMS\Controllers\SilverStripeNavigatorItem;
use SilverStripe\Dev\TestOnly;
class SilverStripeNavigatorTest_TestItem extends SilverStripeNavigatorItem implements TestOnly
{
public function getTitle()
{
return self::class;
}
public function getHTML()
{
return null;
}
}

View File

@ -44,7 +44,7 @@ class LinkablePluginTest extends SapphireTest
public function testResolver()
{
$page = new SiteTree([
$page = SiteTree::create([
'Title' => 'Test page',
'URLSegment' => 'test-page',
'ParentID' => 0,
@ -52,7 +52,7 @@ class LinkablePluginTest extends SapphireTest
$page->write();
$page->publishRecursive();
$page = new SiteTree([
$page = SiteTree::create([
'Title' => 'Other test page',
'URLSegment' => 'other-test-page',
'ParentID' => 0,

View File

@ -2,14 +2,12 @@
namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\CMS\Model\SiteTree;
use Page;
use SilverStripe\CMS\Model\RedirectorPage;
use SilverStripe\CMS\Model\RedirectorPageController;
use SilverStripe\Control\Director;
use SilverStripe\Assets\File;
use SilverStripe\Assets\Dev\TestAssetStore;
use SilverStripe\Control\Controller;
use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\FunctionalTest;
class RedirectorPageTest extends FunctionalTest
@ -38,88 +36,53 @@ class RedirectorPageTest extends FunctionalTest
Director::config()->set('alternate_base_url', 'http://www.mysite.com/');
// Ensure all pages are published
/** @var SiteTree $page */
foreach (SiteTree::get() as $page) {
/** @var Page $page */
foreach (Page::get() as $page) {
$page->publishSingle();
}
}
public function testGoodRedirectors()
{
// For good redirectors, the final destination URL will be returned
$this->assertEquals(
"http://www.google.com",
$this->objFromFixture(RedirectorPage::class, 'goodexternal')->Link()
);
$this->assertEquals(
"/redirection-dest",
$this->objFromFixture(RedirectorPage::class, 'goodinternal')->redirectionLink()
);
$this->assertEquals(
"/redirection-dest",
$this->objFromFixture(RedirectorPage::class, 'goodinternal')->Link()
);
/* For good redirectors, the final destination URL will be returned */
$this->assertEquals("http://www.google.com", $this->objFromFixture(RedirectorPage::class, 'goodexternal')->Link());
$this->assertEquals("/redirection-dest/", $this->objFromFixture(RedirectorPage::class, 'goodinternal')->redirectionLink());
$this->assertEquals("/redirection-dest/", $this->objFromFixture(RedirectorPage::class, 'goodinternal')->Link());
}
public function provideEmptyRedirectors()
public function testEmptyRedirectors()
{
return [
'use 200' => [
'use404' => false,
],
'use 404' => [
'use404' => true,
],
];
}
/**
* @dataProvider provideEmptyRedirectors
*/
public function testEmptyRedirectors(bool $use404)
{
Config::modify()->set(RedirectorPageController::class, 'missing_redirect_is_404', $use404);
// If a redirector page is misconfigured, then its link method will just return the usual
// URLSegment-generated value
/* If a redirector page is misconfigured, then its link method will just return the usual URLSegment-generated value */
$page1 = $this->objFromFixture(RedirectorPage::class, 'badexternal');
$this->assertEquals('/bad-external', $page1->Link());
$response = $this->get($page1->Link());
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('/bad-external/', $page1->Link());
// An error message will be shown if you visit it
/* An error message will be shown if you visit it */
$content = $this->get(Director::makeRelative($page1->Link()))->getBody();
$this->assertStringContainsString('message-setupWithoutRedirect', $content);
// This also applies for internal links
/* This also applies for internal links */
$page2 = $this->objFromFixture(RedirectorPage::class, 'badinternal');
$this->assertEquals('/bad-internal', $page2->Link());
$response = $this->get(Director::makeRelative($page2->Link()));
$content = $response->getBody();
if ($use404) {
$this->assertNull($response->getBody());
} else {
$this->assertStringContainsString('message-setupWithoutRedirect', $content);
}
$this->assertEquals('/bad-internal/', $page2->Link());
$content = $this->get(Director::makeRelative($page2->Link()))->getBody();
$this->assertStringContainsString('message-setupWithoutRedirect', $content);
}
public function testReflexiveAndTransitiveInternalRedirectors()
{
// Reflexive redirectors are those that point to themselves.
// They should behave the same as an empty redirector
/* Reflexive redirectors are those that point to themselves. They should behave the same as an empty redirector */
$page = $this->objFromFixture(RedirectorPage::class, 'reflexive');
$this->assertEquals('/reflexive', $page->Link());
$this->assertEquals('/reflexive/', $page->Link());
$content = $this->get(Director::makeRelative($page->Link()))->getBody();
$this->assertStringContainsString('message-setupWithoutRedirect', $content);
// Transitive redirectors are those that point to another redirector page.
// They should send people to the URLSegment of the destination page - the middle-stop, so to speak.
// That should redirect to the final destination
/* Transitive redirectors are those that point to another redirector page. They should send people to the URLSegment
* of the destination page - the middle-stop, so to speak. That should redirect to the final destination */
$page = $this->objFromFixture(RedirectorPage::class, 'transitive');
$this->assertEquals('/good-internal', $page->Link());
$this->assertEquals('/good-internal/', $page->Link());
$this->autoFollowRedirection = false;
$response = $this->get(Director::makeRelative($page->Link()));
$this->assertEquals(Director::absoluteURL('/redirection-dest'), $response->getHeader("Location"));
$this->assertEquals(Director::absoluteURL('/redirection-dest/'), $response->getHeader("Location"));
}
public function testExternalURLGetsPrefixIfNotSet()
@ -127,11 +90,7 @@ class RedirectorPageTest extends FunctionalTest
$page = $this->objFromFixture(RedirectorPage::class, 'externalnoprefix');
$this->assertEquals($page->ExternalURL, 'http://google.com', 'onBeforeWrite has prefixed with http');
$page->write();
$this->assertEquals(
$page->ExternalURL,
'http://google.com',
'onBeforeWrite will not double prefix if written again!'
);
$this->assertEquals($page->ExternalURL, 'http://google.com', 'onBeforeWrite will not double prefix if written again!');
}
public function testAllowsProtocolRelative()
@ -180,51 +139,4 @@ class RedirectorPageTest extends FunctionalTest
$page = $this->objFromFixture(RedirectorPage::class, 'file');
$this->assertStringContainsString("FileTest.txt", $page->Link());
}
public function provideUnpublishedTarget()
{
return [
'use 200 with sitetree' => [
'use404' => false,
'isFile' => false,
],
'use 404 with sitetree' => [
'use404' => true,
'isFile' => false,
],
'use 200 with file' => [
'use404' => false,
'isFile' => true,
],
'use 404 with file' => [
'use404' => true,
'isFile' => true,
],
];
}
/**
* @dataProvider provideUnpublishedTarget
*/
public function testUnpublishedTarget(bool $use404, bool $isFile)
{
Config::modify()->set(RedirectorPageController::class, 'missing_redirect_is_404', $use404);
$redirectorPage = $this->objFromFixture(RedirectorPage::class, $isFile ? 'file' : 'goodinternal');
$targetModel = $isFile ? $redirectorPage->LinkToFile() : $redirectorPage->LinkTo();
$targetModel->publishSingle();
$redirectorPage->publishSingle();
$this->assertEquals(Controller::normaliseTrailingSlash($isFile ? '/filedirector' : '/good-internal'), $redirectorPage->regularLink());
$redirectorPageLink = Director::makeRelative($redirectorPage->regularLink());
// redirector page should give 301 (redirection) status code
$response = $this->get($redirectorPageLink);
$this->assertEquals(301, $response->getStatusCode());
// Unpublish the target model of this redirector page.
$targetModel->doUnpublish();
// redirector page should give a 404 or a 200 based on config when there's no page to redirect to
$response = $this->get($redirectorPageLink);
$this->assertEquals($use404 ? 404 : 200, $response->getStatusCode());
}
}

View File

@ -1,4 +1,4 @@
SilverStripe\CMS\Model\SiteTree:
Page:
dest:
Title: Redirection Dest
URLSegment: redirection-dest
@ -17,7 +17,7 @@ SilverStripe\CMS\Model\RedirectorPage:
Title: Good Internal
URLSegment: good-internal
RedirectionType: Internal
LinkTo: =>SilverStripe\CMS\Model\SiteTree.dest
LinkTo: =>Page.dest
badexternal:
Title: Bad External
RedirectionType: External

View File

@ -2,6 +2,7 @@
namespace SilverStripe\CMS\Tests\Model;
use Page;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Dev\FunctionalTest;
use SilverStripe\ORM\DB;
@ -20,6 +21,7 @@ use SilverStripe\Versioned\Versioned;
*/
class SiteTreeActionsTest extends FunctionalTest
{
protected static $fixture_file = 'SiteTreeActionsTest.yml';
public function testActionsReadonly()
@ -36,7 +38,7 @@ class SiteTreeActionsTest extends FunctionalTest
Security::setCurrentUser($readonlyEditor);
// Reload latest version
$page = SiteTree::get()->byID($page->ID);
$page = Page::get()->byID($page->ID);
$actions = $page->getCMSActions();
$this->assertNull($actions->dataFieldByName('action_save'));
@ -82,14 +84,14 @@ class SiteTreeActionsTest extends FunctionalTest
$author = $this->objFromFixture(Member::class, 'cmseditor');
Security::setCurrentUser($author);
/** @var SiteTree $page */
$page = new SiteTree();
/** @var Page $page */
$page = new Page();
$page->CanEditType = 'LoggedInUsers';
$page->write();
$page->publishRecursive();
// Reload latest version
$page = SiteTree::get()->byID($page->ID);
$page = Page::get()->byID($page->ID);
$actions = $page->getCMSActions();
@ -106,7 +108,7 @@ class SiteTreeActionsTest extends FunctionalTest
$author = $this->objFromFixture(Member::class, 'cmseditor');
Security::setCurrentUser($author);
$page = new SiteTree();
$page = new Page();
$page->CanEditType = 'LoggedInUsers';
$page->write();
$this->assertTrue($page->canPublish());
@ -133,7 +135,7 @@ class SiteTreeActionsTest extends FunctionalTest
$author = $this->objFromFixture(Member::class, 'cmseditor');
Security::setCurrentUser($author);
$page = new SiteTree();
$page = new Page();
$page->CanEditType = 'LoggedInUsers';
$page->write();
$this->assertTrue($page->canPublish());
@ -143,7 +145,7 @@ class SiteTreeActionsTest extends FunctionalTest
$page->flushCache();
// Reload latest version
$page = SiteTree::get()->byID($page->ID);
$page = Page::get()->byID($page->ID);
$actions = $page->getCMSActions();
$this->assertNotNull($actions->dataFieldByName('action_save'));
@ -156,17 +158,15 @@ class SiteTreeActionsTest extends FunctionalTest
public function testActionsViewingOldVersion()
{
$p = new SiteTree();
$p = new Page();
$p->Content = 'test page first version';
$p->write();
$p->Content = 'new content';
$p->write();
// Looking at the old version, the ability to rollback to that version is available
$version = DB::query(
'SELECT "Version" FROM "SiteTree_Versions" WHERE "Content" = \'test page first version\''
)->value();
$old = Versioned::get_version(SiteTree::class, $p->ID, $version);
$version = DB::query('SELECT "Version" FROM "SiteTree_Versions" WHERE "Content" = \'test page first version\'')->value();
$old = Versioned::get_version('Page', $p->ID, $version);
$actions = $old->getCMSActions();
$this->assertNull($actions->dataFieldByName('action_save'));
$this->assertNull($actions->dataFieldByName('action_publish'));

View File

@ -4,9 +4,9 @@ namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\Dev\TestOnly;
use SilverStripe\Security\Permission;
use SilverStripe\CMS\Model\SiteTree;
use Page;
class SiteTreeActionsTest_Page extends SiteTree implements TestOnly
class SiteTreeActionsTest_Page extends Page implements TestOnly
{
public function canEdit($member = null)
{

View File

@ -18,7 +18,7 @@ class SiteTreeBacklinksTest extends SapphireTest
protected static $required_extensions = [
SiteTree::class => [
SiteTreeBacklinksTest_DOD::class,
SiteTreeBacklinksTest_DOD::class
],
];
@ -34,10 +34,10 @@ class SiteTreeBacklinksTest extends SapphireTest
// testing here.
$this->logInWithPermission('ADMIN');
$page3 = $this->objFromFixture(SiteTree::class, 'page3');
$page3 = $this->objFromFixture('Page', 'page3');
$page3->Content = str_replace(
'$page1.ID',
$this->objFromFixture(SiteTree::class, 'page1')->ID ?? '',
$this->objFromFixture('Page', 'page1')->ID ?? '',
$page3->Content ?? ''
);
$page3->write();
@ -46,22 +46,18 @@ class SiteTreeBacklinksTest extends SapphireTest
public function testSavingPageWithLinkAddsBacklink()
{
// load page 1
$page1 = $this->objFromFixture(SiteTree::class, 'page1');
$page1 = $this->objFromFixture('Page', 'page1');
// assert backlink to page 2 doesn't exist
$page2 = $this->objFromFixture(SiteTree::class, 'page2');
$this->assertNotContains(
$page2->ID,
$page1->BackLinkTracking()->column('ID'),
'Assert backlink to page 2 doesn\'t exist'
);
$page2 = $this->objFromFixture('Page', 'page2');
$this->assertNotContains($page2->ID, $page1->BackLinkTracking()->column('ID'), 'Assert backlink to page 2 doesn\'t exist');
// add hyperlink to page 1 on page 2
$page2->Content .= '<p><a href="[sitetree_link,id=' . $page1->ID . ']">Testing page 1 link</a></p>';
$page2->Content .= '<p><a href="[sitetree_link,id='.$page1->ID.']">Testing page 1 link</a></p>';
$page2->write();
// load page 1
$page1 = $this->objFromFixture(SiteTree::class, 'page1');
$page1 = $this->objFromFixture('Page', 'page1');
// assert backlink to page 2 exists
$this->assertContains($page2->ID, $page1->BackLinkTracking()->column('ID'), 'Assert backlink to page 2 exists');
@ -70,10 +66,10 @@ class SiteTreeBacklinksTest extends SapphireTest
public function testRemovingLinkFromPageRemovesBacklink()
{
// load page 1
$page1 = $this->objFromFixture(SiteTree::class, 'page1');
$page1 = $this->objFromFixture('Page', 'page1');
// assert backlink to page 3 exits
$page3 = $this->objFromFixture(SiteTree::class, 'page3');
$page3 = $this->objFromFixture('Page', 'page3');
$this->assertContains($page3->ID, $page1->BackLinkTracking()->column('ID'), 'Assert backlink to page 3 exists');
// remove hyperlink to page 1
@ -81,156 +77,116 @@ class SiteTreeBacklinksTest extends SapphireTest
$page3->write();
// load page 1
$page1 = $this->objFromFixture(SiteTree::class, 'page1');
$page1 = $this->objFromFixture('Page', 'page1');
// assert backlink to page 3 exists
$this->assertNotContains(
$page3->ID,
$page1->BackLinkTracking()->column('ID'),
'Assert backlink to page 3 doesn\'t exist'
);
$this->assertNotContains($page3->ID, $page1->BackLinkTracking()->column('ID'), 'Assert backlink to page 3 doesn\'t exist');
}
public function testChangingUrlOnDraftSiteRewritesLink()
{
// load page 1
$page1 = $this->objFromFixture(SiteTree::class, 'page1');
$page1 = $this->objFromFixture('Page', 'page1');
// assert backlink to page 3 exists
$page3 = $this->objFromFixture(SiteTree::class, 'page3');
$page3 = $this->objFromFixture('Page', 'page3');
$this->assertContains($page3->ID, $page1->BackLinkTracking()->column('ID'), 'Assert backlink to page 3 exists');
// assert hyperlink to page 1's current url exists on page 3
$links = HTTP::getLinksIn($page3->obj('Content')->forTemplate());
$this->assertContains(
Director::baseURL() . 'page1',
$links,
'Assert hyperlink to page 1\'s current url exists on page 3'
);
$this->assertContains(Director::baseURL().'page1/', $links, 'Assert hyperlink to page 1\'s current url exists on page 3');
// change url of page 1
$page1->URLSegment = 'new-url-segment';
$page1->write();
// load page 3
$page3 = $this->objFromFixture(SiteTree::class, 'page3');
$page3 = $this->objFromFixture('Page', 'page3');
// assert hyperlink to page 1's new url exists
$links = HTTP::getLinksIn($page3->obj('Content')->forTemplate());
$this->assertContains(
Director::baseURL() . 'new-url-segment',
$links,
'Assert hyperlink to page 1\'s new url exists on page 3'
);
$this->assertContains(Director::baseURL().'new-url-segment/', $links, 'Assert hyperlink to page 1\'s new url exists on page 3');
}
public function testChangingUrlOnLiveSiteRewritesLink()
{
// publish page 1 & 3
$page1 = $this->objFromFixture(SiteTree::class, 'page1');
$page3 = $this->objFromFixture(SiteTree::class, 'page3');
$page1 = $this->objFromFixture('Page', 'page1');
$page3 = $this->objFromFixture('Page', 'page3');
$this->assertTrue($page1->publishRecursive());
$this->assertTrue($page3->publishRecursive());
// load pages from live
$page1live = Versioned::get_one_by_stage(SiteTree::class, 'Live', '"SiteTree"."ID" = ' . $page1->ID);
$page3live = Versioned::get_one_by_stage(SiteTree::class, 'Live', '"SiteTree"."ID" = ' . $page3->ID);
$page1live = Versioned::get_one_by_stage('Page', 'Live', '"SiteTree"."ID" = ' . $page1->ID);
$page3live = Versioned::get_one_by_stage('Page', 'Live', '"SiteTree"."ID" = ' . $page3->ID);
// assert backlink to page 3 exists
$this->assertContains(
$page3live->ID,
$page1live->BackLinkTracking()->column('ID'),
'Assert backlink to page 3 exists'
);
$this->assertContains($page3live->ID, $page1live->BackLinkTracking()->column('ID'), 'Assert backlink to page 3 exists');
// assert hyperlink to page 1's current url exists on page 3
$links = HTTP::getLinksIn($page3live->obj('Content')->forTemplate());
$this->assertContains(
Director::baseURL() . 'page1',
$links,
'Assert hyperlink to page 1\'s current url exists on page 3'
);
$this->assertContains(Director::baseURL().'page1/', $links, 'Assert hyperlink to page 1\'s current url exists on page 3');
// change url of page 1
$page1live->URLSegment = 'new-url-segment';
$page1live->writeToStage('Live');
// load page 3 from live
$page3live = Versioned::get_one_by_stage(SiteTree::class, 'Live', '"SiteTree"."ID" = ' . $page3->ID);
$page3live = Versioned::get_one_by_stage('Page', 'Live', '"SiteTree"."ID" = ' . $page3->ID);
// assert hyperlink to page 1's new url exists
Versioned::set_stage(Versioned::LIVE);
$links = HTTP::getLinksIn($page3live->obj('Content')->forTemplate());
$this->assertContains(
Director::baseURL() . 'new-url-segment',
$links,
'Assert hyperlink to page 1\'s new url exists on page 3'
);
$this->assertContains(Director::baseURL().'new-url-segment/', $links, 'Assert hyperlink to page 1\'s new url exists on page 3');
}
public function testPublishingPageWithModifiedUrlRewritesLink()
{
// publish page 1 & 3
$page1 = $this->objFromFixture(SiteTree::class, 'page1');
$page3 = $this->objFromFixture(SiteTree::class, 'page3');
$page1 = $this->objFromFixture('Page', 'page1');
$page3 = $this->objFromFixture('Page', 'page3');
$this->assertTrue($page1->publishRecursive());
$this->assertTrue($page3->publishRecursive());
// load page 3 from live
$page3live = Versioned::get_one_by_stage(SiteTree::class, 'Live', '"SiteTree"."ID" = ' . $page3->ID);
$page3live = Versioned::get_one_by_stage('Page', 'Live', '"SiteTree"."ID" = ' . $page3->ID);
// assert hyperlink to page 1's current url exists
$links = HTTP::getLinksIn($page3live->obj('Content')->forTemplate());
$this->assertContains(
Director::baseURL() . 'page1',
$links,
'Assert hyperlink to page 1\'s current url exists on page 3'
);
$this->assertContains(Director::baseURL().'page1/', $links, 'Assert hyperlink to page 1\'s current url exists on page 3');
// rename url of page 1 on stage
$page1->URLSegment = 'new-url-segment';
$page1->write();
// assert hyperlink to page 1's current publish url exists
$page3live = Versioned::get_one_by_stage(SiteTree::class, 'Live', '"SiteTree"."ID" = ' . $page3->ID);
$page3live = Versioned::get_one_by_stage('Page', 'Live', '"SiteTree"."ID" = ' . $page3->ID);
Versioned::set_stage(Versioned::LIVE);
$links = HTTP::getLinksIn($page3live->obj('Content')->forTemplate());
$this->assertContains(
Director::baseURL() . 'page1',
$links,
'Assert hyperlink to page 1\'s current published url exists on page 3'
);
$this->assertContains(Director::baseURL().'page1/', $links, 'Assert hyperlink to page 1\'s current published url exists on page 3');
// publish page 1
$this->assertTrue($page1->publishRecursive());
// assert hyperlink to page 1's new published url exists
$page3live = Versioned::get_one_by_stage(SiteTree::class, 'Live', '"SiteTree"."ID" = ' . $page3->ID);
$page3live = Versioned::get_one_by_stage('Page', 'Live', '"SiteTree"."ID" = ' . $page3->ID);
$links = HTTP::getLinksIn($page3live->obj('Content')->forTemplate());
$this->assertContains(
Director::baseURL() . 'new-url-segment',
$links,
'Assert hyperlink to page 1\'s new published url exists on page 3'
);
$this->assertContains(Director::baseURL().'new-url-segment/', $links, 'Assert hyperlink to page 1\'s new published url exists on page 3');
}
public function testPublishingPageWithModifiedLinksRewritesLinks()
{
// publish page 1 & 3
$page1 = $this->objFromFixture(SiteTree::class, 'page1');
$page3 = $this->objFromFixture(SiteTree::class, 'page3');
$page1 = $this->objFromFixture('Page', 'page1');
$page3 = $this->objFromFixture('Page', 'page3');
$this->assertTrue($page1->publishRecursive());
$this->assertTrue($page3->publishRecursive());
// assert hyperlink to page 1's current url exists
$links = HTTP::getLinksIn($page3->obj('Content')->forTemplate());
$this->assertContains(
Director::baseURL() . 'page1',
$links,
'Assert hyperlink to page 1\'s current published url exists on page 3'
);
$this->assertContains(Director::baseURL().'page1/', $links, 'Assert hyperlink to page 1\'s current published url exists on page 3');
// change page 1 url on draft
$page1->URLSegment = 'new-url-segment';
@ -239,58 +195,42 @@ class SiteTreeBacklinksTest extends SapphireTest
$page1->write();
// assert page 3 on draft contains new page 1 url
$page3 = $this->objFromFixture(SiteTree::class, 'page3');
$page3 = $this->objFromFixture('Page', 'page3');
$links = HTTP::getLinksIn($page3->obj('Content')->forTemplate());
$this->assertContains(
Director::baseURL() . 'new-url-segment',
$links,
'Assert hyperlink to page 1\'s current draft url exists on page 3'
);
$this->assertContains(Director::baseURL().'new-url-segment/', $links, 'Assert hyperlink to page 1\'s current draft url exists on page 3');
// publish page 3
$this->assertTrue($page3->publishRecursive());
// assert page 3 on published site contains old page 1 url
$page3live = Versioned::get_one_by_stage(SiteTree::class, 'Live', '"SiteTree"."ID" = ' . $page3->ID);
$page3live = Versioned::get_one_by_stage('Page', 'Live', '"SiteTree"."ID" = ' . $page3->ID);
Versioned::set_stage(Versioned::LIVE);
$links = HTTP::getLinksIn($page3live->obj('Content')->forTemplate());
$this->assertContains(
Director::baseURL() . 'page1',
$links,
'Assert hyperlink to page 1\'s current published url exists on page 3'
);
$this->assertContains(Director::baseURL().'page1/', $links, 'Assert hyperlink to page 1\'s current published url exists on page 3');
// publish page 1
$this->assertTrue($page1->publishRecursive());
// assert page 3 on published site contains new page 1 url
$page3live = Versioned::get_one_by_stage(SiteTree::class, 'Live', '"SiteTree"."ID" = ' . $page3->ID);
$page3live = Versioned::get_one_by_stage('Page', 'Live', '"SiteTree"."ID" = ' . $page3->ID);
$links = HTTP::getLinksIn($page3live->obj('Content')->forTemplate());
$this->assertContains(
Director::baseURL() . 'new-url-segment',
$links,
'Assert hyperlink to page 1\'s current published url exists on page 3'
);
$this->assertContains(Director::baseURL().'new-url-segment/', $links, 'Assert hyperlink to page 1\'s current published url exists on page 3');
}
public function testLinkTrackingOnExtraContentFields()
{
/** @var SiteTree $page1 */
$page1 = $this->objFromFixture(SiteTree::class, 'page1');
/** @var SiteTree $page2 */
$page2 = $this->objFromFixture(SiteTree::class, 'page2');
/** @var Page $page1 */
$page1 = $this->objFromFixture('Page', 'page1');
/** @var Page $page2 */
$page2 = $this->objFromFixture('Page', 'page2');
$page1->publishRecursive();
$page2->publishRecursive();
// assert backlink to page 2 doesn't exist
$this->assertNotContains(
$page2->ID,
$page1->BackLinkTracking()->column('ID'),
'Assert backlink to page 2 doesn\'t exist'
);
$this->assertNotContains($page2->ID, $page1->BackLinkTracking()->column('ID'), 'Assert backlink to page 2 doesn\'t exist');
// add hyperlink to page 1 on page 2
$page2->ExtraContent .= '<p><a href="[sitetree_link,id=' . $page1->ID . ']">Testing page 1 link</a></p>';
$page2->ExtraContent .= '<p><a href="[sitetree_link,id='.$page1->ID.']">Testing page 1 link</a></p>';
$page2->write();
$page2->publishRecursive();
@ -298,32 +238,23 @@ class SiteTreeBacklinksTest extends SapphireTest
$this->assertContains($page2->ID, $page1->BackLinkTracking()->column('ID'), 'Assert backlink to page 2 exists');
// update page1 url
$page1 = $this->objFromFixture(SiteTree::class, 'page1');
$page1 = $this->objFromFixture('Page', 'page1');
$page1->URLSegment = "page1-new-url";
$page1->write();
// confirm that draft link on page2 has been rewritten
$page2 = $this->objFromFixture(SiteTree::class, 'page2');
$this->assertEquals(
'<p><a href="' . Director::baseURL() . 'page1-new-url">Testing page 1 link</a></p>',
$page2->obj('ExtraContent')->forTemplate()
);
$page2 = $this->objFromFixture('Page', 'page2');
$this->assertEquals('<p><a href="'.Director::baseURL().'page1-new-url/">Testing page 1 link</a></p>', $page2->obj('ExtraContent')->forTemplate());
// confirm that published link hasn't
$page2Live = Versioned::get_one_by_stage(SiteTree::class, "Live", "\"SiteTree\".\"ID\" = $page2->ID");
$page2Live = Versioned::get_one_by_stage("Page", "Live", "\"SiteTree\".\"ID\" = $page2->ID");
Versioned::set_stage(Versioned::LIVE);
$this->assertEquals(
'<p><a href="' . Director::baseURL() . 'page1">Testing page 1 link</a></p>',
$page2Live->obj('ExtraContent')->forTemplate()
);
$this->assertEquals('<p><a href="'.Director::baseURL().'page1/">Testing page 1 link</a></p>', $page2Live->obj('ExtraContent')->forTemplate());
// publish page1 and confirm that the link on the published page2 has now been updated
$page1->publishRecursive();
$page2Live = Versioned::get_one_by_stage(SiteTree::class, "Live", "\"SiteTree\".\"ID\" = $page2->ID");
$this->assertEquals(
'<p><a href="' . Director::baseURL() . 'page1-new-url">Testing page 1 link</a></p>',
$page2Live->obj('ExtraContent')->forTemplate()
);
$page2Live = Versioned::get_one_by_stage("Page", "Live", "\"SiteTree\".\"ID\" = $page2->ID");
$this->assertEquals('<p><a href="'.Director::baseURL().'page1-new-url/">Testing page 1 link</a></p>', $page2Live->obj('ExtraContent')->forTemplate());
// Edit draft again
Versioned::set_stage(Versioned::DRAFT);
@ -331,16 +262,12 @@ class SiteTreeBacklinksTest extends SapphireTest
$page2->write();
// assert backlink to page 2 no longer exists
$this->assertNotContains(
$page2->ID,
$page1->BackLinkTracking()->column('ID'),
'Assert backlink to page 2 has been removed'
);
$this->assertNotContains($page2->ID, $page1->BackLinkTracking()->column('ID'), 'Assert backlink to page 2 has been removed');
}
public function testLinkTrackingWithUntitledObjectsDisplaysAReadableIdentifier()
{
$page = $this->objFromFixture(SiteTree::class, 'page2');
$page = $this->objFromFixture('Page', 'page2');
$referencingObject = new SiteTreeBacklinksTestContentObject();
$referencingObject->Content = '<p><a href="[sitetree_link,id='

View File

@ -1,4 +1,4 @@
SilverStripe\CMS\Model\SiteTree:
Page:
page1:
Title: page1
URLSegment: page1
@ -11,6 +11,6 @@ SilverStripe\CMS\Model\SiteTree:
Title: page3
URLSegment: page3
Content: '<p><a href="[sitetree_link,id=$page1.ID]">Testing page 1 link</a></p>'
LinkTracking: =>SilverStripe\CMS\Model\SiteTree.page1
LinkTracking: =>Page.page1

View File

@ -2,6 +2,7 @@
namespace SilverStripe\CMS\Tests\Model;
use Page;
use Silverstripe\Assets\Dev\TestAssetStore;
use SilverStripe\CMS\Model\RedirectorPage;
use SilverStripe\CMS\Model\SiteTree;
@ -41,15 +42,15 @@ class SiteTreeBrokenLinksTest extends SapphireTest
public function testBrokenLinksBetweenPages()
{
/** @var SiteTree $obj */
$obj = $this->objFromFixture(SiteTree::class, 'content');
/** @var Page $obj */
$obj = $this->objFromFixture('Page', 'content');
$obj->Content = '<a href="[sitetree_link,id=3423423]">this is a broken link</a>';
$obj->syncLinkTracking();
$this->assertTrue($obj->HasBrokenLink, 'Page has a broken link');
$obj->Content = '<a href="[sitetree_link,id=' . $this->idFromFixture(
SiteTree::class,
'Page',
'about'
) . ']">this is not a broken link</a>';
$obj->syncLinkTracking();
@ -61,8 +62,8 @@ class SiteTreeBrokenLinksTest extends SapphireTest
*/
public function testBrokenLinksNonPage()
{
/** @var SiteTree $aboutPage */
$aboutPage = $this->objFromFixture(SiteTree::class, 'about');
/** @var Page $aboutPage */
$aboutPage = $this->objFromFixture('Page', 'about');
/** @var NotPageObject $obj */
$obj = $this->objFromFixture(NotPageObject::class, 'object1');
@ -93,7 +94,7 @@ class SiteTreeBrokenLinksTest extends SapphireTest
// About-page backlinks contains this object
$this->assertListEquals(
[
['ID' => $obj->ID],
['ID' => $obj->ID]
],
$aboutPage->BackLinkTracking()
);
@ -101,9 +102,9 @@ class SiteTreeBrokenLinksTest extends SapphireTest
public function testBrokenAnchorBetweenPages()
{
/** @var SiteTree $obj */
$obj = $this->objFromFixture(SiteTree::class, 'content');
$target = $this->objFromFixture(SiteTree::class, 'about');
/** @var Page $obj */
$obj = $this->objFromFixture('Page', 'content');
$target = $this->objFromFixture('Page', 'about');
$obj->Content = "<a href=\"[sitetree_link,id={$target->ID}]#no-anchor-here\">this is a broken link</a>";
$obj->syncLinkTracking();
@ -116,7 +117,7 @@ class SiteTreeBrokenLinksTest extends SapphireTest
public function testBrokenVirtualPages()
{
$obj = $this->objFromFixture(SiteTree::class, 'content');
$obj = $this->objFromFixture('Page', 'content');
$vp = new VirtualPage();
$vp->CopyContentFromID = $obj->ID;
@ -130,7 +131,7 @@ class SiteTreeBrokenLinksTest extends SapphireTest
public function testBrokenInternalRedirectorPages()
{
$obj = $this->objFromFixture(SiteTree::class, 'content');
$obj = $this->objFromFixture('Page', 'content');
$rp = new RedirectorPage();
$rp->RedirectionType = 'Internal';
@ -147,10 +148,10 @@ class SiteTreeBrokenLinksTest extends SapphireTest
public function testDeletingMarksBackLinkedPagesAsBroken()
{
// Set up two published pages with a link from content -> about
$linkDest = $this->objFromFixture(SiteTree::class, 'about');
$linkDest = $this->objFromFixture('Page', 'about');
/** @var SiteTree $linkSrc */
$linkSrc = $this->objFromFixture(SiteTree::class, 'content');
/** @var Page $linkSrc */
$linkSrc = $this->objFromFixture('Page', 'content');
$linkSrc->Content = "<p><a href=\"[sitetree_link,id=$linkDest->ID]\">about us</a></p>";
$linkSrc->write();
@ -162,7 +163,7 @@ class SiteTreeBrokenLinksTest extends SapphireTest
// Confirm draft has broken link
$linkSrc->flushCache();
$linkSrc = $this->objFromFixture(SiteTree::class, 'content');
$linkSrc = $this->objFromFixture('Page', 'content');
$this->assertEquals(1, (int)$linkSrc->HasBrokenLink);
}
@ -172,13 +173,13 @@ class SiteTreeBrokenLinksTest extends SapphireTest
$this->logInWithPermission('ADMIN');
// Set up two draft pages with a link from content -> about
/** @var SiteTree $linkDest */
$linkDest = $this->objFromFixture(SiteTree::class, 'about');
/** @var Page $linkDest */
$linkDest = $this->objFromFixture('Page', 'about');
// Ensure that it's not on the published site
$linkDest->doUnpublish();
/** @var SiteTree $linkSrc */
$linkSrc = $this->objFromFixture(SiteTree::class, 'content');
/** @var Page $linkSrc */
$linkSrc = $this->objFromFixture('Page', 'content');
$linkSrc->Content = "<p><a href=\"[sitetree_link,id=$linkDest->ID]\">about us</a></p>";
$linkSrc->write();
@ -190,20 +191,20 @@ class SiteTreeBrokenLinksTest extends SapphireTest
// Live doesn't have separate broken link tracking
$this->assertEquals(0, DB::query("SELECT \"HasBrokenLink\" FROM \"SiteTree_Live\"
WHERE \"ID\" = $linkSrc->ID")->value());
WHERE \"ID\" = $linkSrc->ID")->value());
}
public function testRestoreFixesBrokenLinks()
{
// Create page and virtual page
$p = new SiteTree();
$p = new Page();
$p->Title = "source";
$p->write();
$pageID = $p->ID;
$this->assertTrue($p->publishRecursive());
// Content links are one kind of link to pages
$p2 = new SiteTree();
$p2 = new Page();
$p2->Title = "regular link";
$p2->Content = "<a href=\"[sitetree_link,id=$p->ID]\">test</a>";
$p2->write();
@ -263,14 +264,14 @@ class SiteTreeBrokenLinksTest extends SapphireTest
public function testRevertToLiveFixesBrokenLinks()
{
// Create page and virutal page
$page = new SiteTree();
$page = new Page();
$page->Title = "source";
$page->write();
$pageID = $page->ID;
$this->assertTrue($page->publishRecursive());
// Content links are one kind of link to pages
$page2 = new SiteTree();
$page2 = new Page();
$page2->Title = "regular link";
$page2->Content = "<a href=\"[sitetree_link,id={$pageID}]\">test</a>";
$page2->write();
@ -301,7 +302,7 @@ class SiteTreeBrokenLinksTest extends SapphireTest
$this->assertEquals(1, $redirectorPage->HasBrokenLink);
// Call doRevertToLive and confirm that broken links are restored
/** @var SiteTree $pageLive */
/** @var Page $pageLive */
$pageLive = Versioned::get_one_by_stage(SiteTree::class, 'Live', '"SiteTree"."ID" = ' . $pageID);
$pageLive->doRevertToLive();
@ -315,17 +316,15 @@ class SiteTreeBrokenLinksTest extends SapphireTest
public function testBrokenAnchorLinksInAPage()
{
/** @var SiteTree $obj */
$obj = $this->objFromFixture(SiteTree::class, 'content');
/** @var Page $obj */
$obj = $this->objFromFixture('Page', 'content');
$origContent = $obj->Content;
$obj->Content = $origContent . '<a href="#no-anchor-here">this links to a non-existent in-page anchor or ' .
'skiplink</a>';
$obj->Content = $origContent . '<a href="#no-anchor-here">this links to a non-existent in-page anchor or skiplink</a>';
$obj->syncLinkTracking();
$this->assertTrue($obj->HasBrokenLink, 'Page has a broken anchor/skiplink');
$obj->Content = $origContent . '<a href="#yes-anchor-here">this links to an existent in-page ' .
'anchor/skiplink</a>';
$obj->Content = $origContent . '<a href="#yes-anchor-here">this links to an existent in-page anchor/skiplink</a>';
$obj->syncLinkTracking();
$this->assertFalse($obj->HasBrokenLink, 'Page doesn\'t have a broken anchor or skiplink');
}

View File

@ -1,4 +1,4 @@
SilverStripe\CMS\Model\SiteTree:
Page:
content:
Title: ContentPage
Content: 'This is some partially happy content. It has one missing a skiplink, but does have another <a name="yes-anchor-here">skiplink here</a>.'
@ -13,7 +13,7 @@ SilverStripe\CMS\Model\SiteTree:
workingInternalRedirector:
RedirectionType: Internal
Title: RedirectorPageToBrokenInteralPage
LinkTo: =>SilverStripe\CMS\Model\SiteTree.content
LinkTo: =>Page.content
SilverStripe\CMS\Tests\Model\SiteTreeBrokenLinksTest\NotPageObject:
object1:
Content: 'Everything will be ok'

View File

@ -2,6 +2,7 @@
namespace SilverStripe\CMS\Tests\Model;
use Page;
use SilverStripe\Assets\Dev\TestAssetStore;
use SilverStripe\Assets\File;
use SilverStripe\Assets\Filesystem;
@ -30,8 +31,8 @@ class SiteTreeHTMLEditorFieldTest extends FunctionalTest
}
// Ensure all pages are published
/** @var SiteTree $page */
foreach (SiteTree::get() as $page) {
/** @var Page $page */
foreach (Page::get() as $page) {
$page->publishSingle();
}
}
@ -54,11 +55,7 @@ class SiteTreeHTMLEditorFieldTest extends FunctionalTest
$editor->setValue("<a href=\"[sitetree_link,id=$aboutID]\">Example Link</a>");
$editor->saveInto($sitetree);
$sitetree->write();
$this->assertEquals(
[$aboutID => $aboutID],
$sitetree->LinkTracking()->getIdList(),
'Basic link tracking works.'
);
$this->assertEquals([$aboutID => $aboutID], $sitetree->LinkTracking()->getIdList(), 'Basic link tracking works.');
$editor->setValue(
"<a href=\"[sitetree_link,id=$aboutID]\"></a><a href=\"[sitetree_link,id=$contactID]\"></a>"

View File

@ -2,6 +2,7 @@
namespace SilverStripe\CMS\Tests\Model;
use Page;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\CMS\Model\SiteTreeLinkTracking_Parser;
use SilverStripe\Control\Director;
@ -31,7 +32,7 @@ class SiteTreeLinkTrackingTest extends SapphireTest
public function testParser()
{
SiteTree::add_extension(SiteTree::class, SiteTreeLinkTracking_Extension::class);
SiteTree::add_extension(Page::class, SiteTreeLinkTracking_Extension::class);
// Shortcodes
$this->assertTrue($this->isBroken('<a href="[sitetree_link,id=123]">link</a>'));
@ -54,7 +55,8 @@ class SiteTreeLinkTrackingTest extends SapphireTest
$this->assertFalse($this->isBroken('<a id="anchor">anchor</a>'));
$this->assertTrue($this->isBroken('<a href="##anchor">anchor</a>'));
$page = new SiteTree();
$page = new Page();
$page->Content = '<a name="yes-name-anchor">name</a><a id="yes-id-anchor">id</a>';
$page->write();
@ -70,7 +72,7 @@ class SiteTreeLinkTrackingTest extends SapphireTest
protected function highlight($content)
{
$page = new SiteTree();
$page = new Page();
$page->Content = $content;
$page->write();
return $page->Content;
@ -85,7 +87,7 @@ class SiteTreeLinkTrackingTest extends SapphireTest
$content = $this->highlight('<a href="[sitetree_link,id=123]">link</a>');
$this->assertEquals(substr_count($content ?? '', 'ss-broken'), 1, 'ss-broken class is added to the broken link.');
$otherPage = new SiteTree();
$otherPage = new Page();
$otherPage->Content = '';
$otherPage->write();

View File

@ -2,6 +2,7 @@
namespace SilverStripe\CMS\Tests\Model;
use Page;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Dev\FunctionalTest;
@ -12,6 +13,10 @@ use SilverStripe\SiteConfig\SiteConfig;
use SilverStripe\Subsites\Extensions\SiteTreeSubsites;
use SilverStripe\Versioned\Versioned;
/**
* @todo Test canAddChildren()
* @todo Test canCreate()
*/
class SiteTreePermissionsTest extends FunctionalTest
{
protected static $fixture_file = "SiteTreePermissionsTest.yml";
@ -28,8 +33,8 @@ class SiteTreePermissionsTest extends FunctionalTest
$this->autoFollowRedirection = false;
// Ensure all pages are published
/** @var SiteTree $page */
foreach (SiteTree::get() as $page) {
/** @var Page $page */
foreach (Page::get() as $page) {
if ($page->URLSegment !== 'draft-only') {
$page->publishSingle();
}
@ -41,8 +46,8 @@ class SiteTreePermissionsTest extends FunctionalTest
{
$this->autoFollowRedirection = false;
/** @var SiteTree $draftOnlyPage */
$draftOnlyPage = $this->objFromFixture(SiteTree::class, 'draftOnlyPage');
/** @var Page $draftOnlyPage */
$draftOnlyPage = $this->objFromFixture(Page::class, 'draftOnlyPage');
$this->logOut();
$response = $this->get($draftOnlyPage->URLSegment . '?stage=Live');
@ -80,7 +85,7 @@ class SiteTreePermissionsTest extends FunctionalTest
{
// Set up fixture - a published page deleted from draft
$this->logInWithPermission("ADMIN");
$page = $this->objFromFixture(SiteTree::class, 'restrictedEditOnlySubadminGroup');
$page = $this->objFromFixture(Page::class, 'restrictedEditOnlySubadminGroup');
$pageID = $page->ID;
$this->assertTrue($page->publishRecursive());
$page->delete();
@ -107,7 +112,7 @@ class SiteTreePermissionsTest extends FunctionalTest
{
// Set up fixture - an unpublished page
$this->logInWithPermission("ADMIN");
$page = $this->objFromFixture(SiteTree::class, 'restrictedEditOnlySubadminGroup');
$page = $this->objFromFixture(Page::class, 'restrictedEditOnlySubadminGroup');
$pageID = $page->ID;
$page->doUnpublish();
@ -130,7 +135,7 @@ class SiteTreePermissionsTest extends FunctionalTest
{
// Find a page that exists and delete it from both stage and published
$this->logInWithPermission("ADMIN");
$page = $this->objFromFixture(SiteTree::class, 'restrictedEditOnlySubadminGroup');
$page = $this->objFromFixture(Page::class, 'restrictedEditOnlySubadminGroup');
$pageID = $page->ID;
$page->doUnpublish();
$page->delete();
@ -148,8 +153,8 @@ class SiteTreePermissionsTest extends FunctionalTest
public function testCanViewStage()
{
// Get page & make sure it exists on Live
/** @var SiteTree $page */
$page = $this->objFromFixture(SiteTree::class, 'standardpage');
/** @var Page $page */
$page = $this->objFromFixture(Page::class, 'standardpage');
$page->publishSingle();
// Then make sure there's a new version on Stage
@ -168,7 +173,7 @@ class SiteTreePermissionsTest extends FunctionalTest
public function testAccessTabOnlyDisplaysWithGrantAccessPermissions()
{
$page = $this->objFromFixture(SiteTree::class, 'standardpage');
$page = $this->objFromFixture(Page::class, 'standardpage');
$subadminuser = $this->objFromFixture(Member::class, 'subadmin');
Security::setCurrentUser($subadminuser);
@ -199,7 +204,7 @@ class SiteTreePermissionsTest extends FunctionalTest
public function testRestrictedViewLoggedInUsers()
{
$page = $this->objFromFixture(SiteTree::class, 'restrictedViewLoggedInUsers');
$page = $this->objFromFixture(Page::class, 'restrictedViewLoggedInUsers');
// unauthenticated users
$this->assertFalse(
@ -218,23 +223,21 @@ class SiteTreePermissionsTest extends FunctionalTest
$websiteuser = $this->objFromFixture(Member::class, 'websiteuser');
$this->assertTrue(
$page->canView($websiteuser),
'Authenticated members can view a page marked as "Viewable for any logged in users" even if they dont ' .
'have access to the CMS'
'Authenticated members can view a page marked as "Viewable for any logged in users" even if they dont have access to the CMS'
);
$this->logInAs($websiteuser);
$response = $this->get($page->RelativeLink());
$this->assertEquals(
$response->getStatusCode(),
200,
'Authenticated members can view a page marked as "Viewable for any logged in users" even if they dont ' .
'have access to the CMS'
'Authenticated members can view a page marked as "Viewable for any logged in users" even if they dont have access to the CMS'
);
$this->logOut();
}
public function testRestrictedViewOnlyTheseUsers()
{
$page = $this->objFromFixture(SiteTree::class, 'restrictedViewOnlyWebsiteUsers');
$page = $this->objFromFixture(Page::class, 'restrictedViewOnlyWebsiteUsers');
// unauthenticcated users
$this->assertFalse(
@ -253,16 +256,14 @@ class SiteTreePermissionsTest extends FunctionalTest
$subadminuser = $this->objFromFixture(Member::class, 'subadmin');
$this->assertFalse(
$page->canView($subadminuser),
'Authenticated members cant view a page marked as "Viewable by these groups" if theyre not in the listed ' .
'groups'
'Authenticated members cant view a page marked as "Viewable by these groups" if theyre not in the listed groups'
);
$this->LogInAs($subadminuser);
$response = $this->get($page->RelativeLink());
$this->assertEquals(
$response->getStatusCode(),
403,
'Authenticated members cant view a page marked as "Viewable by these groups" if theyre not in the listed ' .
'groups'
'Authenticated members cant view a page marked as "Viewable by these groups" if theyre not in the listed groups'
);
$this->logOut();
@ -284,7 +285,7 @@ class SiteTreePermissionsTest extends FunctionalTest
public function testRestrictedEditLoggedInUsers()
{
$page = $this->objFromFixture(SiteTree::class, 'restrictedEditLoggedInUsers');
$page = $this->objFromFixture(Page::class, 'restrictedEditLoggedInUsers');
// unauthenticcated users
$this->assertFalse(
@ -297,22 +298,20 @@ class SiteTreePermissionsTest extends FunctionalTest
Security::setCurrentUser($websiteuser);
$this->assertFalse(
$page->canEdit($websiteuser),
'Authenticated members cant edit a page marked as "Editable by logged in users" if they dont have cms ' .
'permissions'
'Authenticated members cant edit a page marked as "Editable by logged in users" if they dont have cms permissions'
);
// subadmin users
$subadminuser = $this->objFromFixture(Member::class, 'subadmin');
$this->assertTrue(
$page->canEdit($subadminuser),
'Authenticated members can edit a page marked as "Editable by logged in users" if they have cms ' .
'permissions and belong to any of these groups'
'Authenticated members can edit a page marked as "Editable by logged in users" if they have cms permissions and belong to any of these groups'
);
}
public function testRestrictedEditOnlySubadminGroup()
{
$page = $this->objFromFixture(SiteTree::class, 'restrictedEditOnlySubadminGroup');
$page = $this->objFromFixture(Page::class, 'restrictedEditOnlySubadminGroup');
// unauthenticated users
$this->assertFalse(
@ -331,15 +330,14 @@ class SiteTreePermissionsTest extends FunctionalTest
$websiteuser = $this->objFromFixture(Member::class, 'websiteuser');
$this->assertFalse(
$page->canEdit($websiteuser),
'Authenticated members cant edit a page marked as "Editable by these groups" if theyre not in the listed ' .
'groups'
'Authenticated members cant edit a page marked as "Editable by these groups" if theyre not in the listed groups'
);
}
public function testRestrictedViewInheritance()
{
$parentPage = $this->objFromFixture(SiteTree::class, 'parent_restrictedViewOnlySubadminGroup');
$childPage = $this->objFromFixture(SiteTree::class, 'child_restrictedViewOnlySubadminGroup');
$parentPage = $this->objFromFixture(Page::class, 'parent_restrictedViewOnlySubadminGroup');
$childPage = $this->objFromFixture(Page::class, 'child_restrictedViewOnlySubadminGroup');
// unauthenticated users
$this->assertFalse(
@ -358,24 +356,22 @@ class SiteTreePermissionsTest extends FunctionalTest
$subadminuser = $this->objFromFixture(Member::class, 'subadmin');
$this->assertTrue(
$childPage->canView($subadminuser),
'Authenticated members can view a page marked as "Viewable by these groups" if theyre in the listed ' .
'groups by inherited permission'
'Authenticated members can view a page marked as "Viewable by these groups" if theyre in the listed groups by inherited permission'
);
$this->logInAs($subadminuser);
$response = $this->get($childPage->RelativeLink());
$this->assertEquals(
$response->getStatusCode(),
200,
'Authenticated members can view a page marked as "Viewable by these groups" if theyre in the listed ' .
'groups by inherited permission'
'Authenticated members can view a page marked as "Viewable by these groups" if theyre in the listed groups by inherited permission'
);
$this->logOut();
}
public function testRestrictedEditInheritance()
{
$parentPage = $this->objFromFixture(SiteTree::class, 'parent_restrictedEditOnlySubadminGroup');
$childPage = $this->objFromFixture(SiteTree::class, 'child_restrictedEditOnlySubadminGroup');
$parentPage = $this->objFromFixture(Page::class, 'parent_restrictedEditOnlySubadminGroup');
$childPage = $this->objFromFixture(Page::class, 'child_restrictedEditOnlySubadminGroup');
// unauthenticated users
$this->assertFalse(
@ -387,15 +383,14 @@ class SiteTreePermissionsTest extends FunctionalTest
$subadminuser = $this->objFromFixture(Member::class, 'subadmin');
$this->assertTrue(
$childPage->canEdit($subadminuser),
'Authenticated members can edit a page marked as "Editable by these groups" if theyre in the listed ' .
'groups by inherited permission'
'Authenticated members can edit a page marked as "Editable by these groups" if theyre in the listed groups by inherited permission'
);
}
public function testDeleteRestrictedChild()
{
$parentPage = $this->objFromFixture(SiteTree::class, 'deleteTestParentPage');
$childPage = $this->objFromFixture(SiteTree::class, 'deleteTestChildPage');
$parentPage = $this->objFromFixture(Page::class, 'deleteTestParentPage');
$childPage = $this->objFromFixture(Page::class, 'deleteTestChildPage');
// unauthenticated users
$this->assertFalse(
@ -410,7 +405,7 @@ class SiteTreePermissionsTest extends FunctionalTest
public function testRestrictedEditLoggedInUsersDeletedFromStage()
{
$page = $this->objFromFixture(SiteTree::class, 'restrictedEditLoggedInUsers');
$page = $this->objFromFixture(Page::class, 'restrictedEditLoggedInUsers');
$pageID = $page->ID;
$this->logInWithPermission("ADMIN");
@ -426,60 +421,39 @@ class SiteTreePermissionsTest extends FunctionalTest
$subadminuser = $this->objFromFixture(Member::class, 'subadmin');
$this->assertTrue(
$page->canEdit($subadminuser),
'Authenticated members can edit a page that was deleted from stage and marked as "Editable by logged ' .
'in users" if they have cms permissions and belong to any of these groups'
'Authenticated members can edit a page that was deleted from stage and marked as "Editable by logged in users" if they have cms permissions and belong to any of these groups'
);
}
public function testInheritCanViewFromSiteConfig()
{
$page = $this->objFromFixture(SiteTree::class, 'inheritWithNoParent');
$page = $this->objFromFixture(Page::class, 'inheritWithNoParent');
$siteconfig = $this->objFromFixture(SiteConfig::class, 'default');
$editor = $this->objFromFixture(Member::class, 'editor');
$editorGroup = $this->objFromFixture(Group::class, 'editorgroup');
$siteconfig->CanViewType = 'Anyone';
$siteconfig->write();
$this->assertTrue(
$page->canView(false),
'Anyone can view a page when set to inherit from the SiteConfig, and SiteConfig has canView set to ' .
'LoggedInUsers'
);
$this->assertTrue($page->canView(false), 'Anyone can view a page when set to inherit from the SiteConfig, and SiteConfig has canView set to LoggedInUsers');
$siteconfig->CanViewType = 'LoggedInUsers';
$siteconfig->write();
$this->assertFalse(
$page->canView(false),
'Anonymous can\'t view a page when set to inherit from the SiteConfig, and SiteConfig has canView set to ' .
'LoggedInUsers'
);
$this->assertFalse($page->canView(false), 'Anonymous can\'t view a page when set to inherit from the SiteConfig, and SiteConfig has canView set to LoggedInUsers');
$siteconfig->CanViewType = 'LoggedInUsers';
$siteconfig->write();
$this->assertTrue(
$page->canView($editor),
'Users can view a page when set to inherit from the SiteConfig, and SiteConfig has canView set to ' .
'LoggedInUsers'
);
$this->assertTrue($page->canView($editor), 'Users can view a page when set to inherit from the SiteConfig, and SiteConfig has canView set to LoggedInUsers');
$siteconfig->CanViewType = 'OnlyTheseUsers';
$siteconfig->ViewerGroups()->add($editorGroup);
$siteconfig->write();
$this->assertTrue(
$page->canView($editor),
'Editors can view a page when set to inherit from the SiteConfig, and SiteConfig has canView set to ' .
'OnlyTheseUsers'
);
$this->assertFalse(
$page->canView(false),
'Anonymous can\'t view a page when set to inherit from the SiteConfig, and SiteConfig has canView set ' .
'to OnlyTheseUsers'
);
$this->assertTrue($page->canView($editor), 'Editors can view a page when set to inherit from the SiteConfig, and SiteConfig has canView set to OnlyTheseUsers');
$this->assertFalse($page->canView(false), 'Anonymous can\'t view a page when set to inherit from the SiteConfig, and SiteConfig has canView set to OnlyTheseUsers');
}
public function testInheritCanEditFromSiteConfig()
{
$page = $this->objFromFixture(SiteTree::class, 'inheritWithNoParent');
$page = $this->objFromFixture(Page::class, 'inheritWithNoParent');
$siteconfig = $this->objFromFixture(SiteConfig::class, 'default');
$editor = $this->objFromFixture(Member::class, 'editor');
$user = $this->objFromFixture(Member::class, 'websiteuser');
@ -488,99 +462,17 @@ class SiteTreePermissionsTest extends FunctionalTest
$siteconfig->CanEditType = 'LoggedInUsers';
$siteconfig->write();
$this->assertFalse(
$page->canEdit(false),
'Anonymous can\'t edit a page when set to inherit from the SiteConfig, and SiteConfig has canEdit set ' .
'to LoggedInUsers'
);
$this->assertFalse($page->canEdit(false), 'Anonymous can\'t edit a page when set to inherit from the SiteConfig, and SiteConfig has canEdit set to LoggedInUsers');
Security::setCurrentUser($editor);
$this->assertTrue(
$page->canEdit(),
'Users can edit a page when set to inherit from the SiteConfig, and SiteConfig has canEdit set to ' .
'LoggedInUsers'
);
$this->assertTrue($page->canEdit(), 'Users can edit a page when set to inherit from the SiteConfig, and SiteConfig has canEdit set to LoggedInUsers');
$siteconfig->CanEditType = 'OnlyTheseUsers';
$siteconfig->EditorGroups()->add($editorGroup);
$siteconfig->write();
$this->assertTrue(
$page->canEdit($editor),
'Editors can edit a page when set to inherit from the SiteConfig, and SiteConfig has canEdit set to ' .
'OnlyTheseUsers'
);
$this->assertTrue($page->canEdit($editor), 'Editors can edit a page when set to inherit from the SiteConfig, and SiteConfig has canEdit set to OnlyTheseUsers');
Security::setCurrentUser(null);
$this->assertFalse(
$page->canEdit(false),
'Anonymous can\'t edit a page when set to inherit from the SiteConfig, and SiteConfig has canEdit set ' .
'to OnlyTheseUsers'
);
$this->assertFalse($page->canEdit(false), 'Anonymous can\'t edit a page when set to inherit from the SiteConfig, and SiteConfig has canEdit set to OnlyTheseUsers');
Security::setCurrentUser($user);
$this->assertFalse(
$page->canEdit($user),
'Website user can\'t edit a page when set to inherit from the SiteConfig, and SiteConfig has canEdit set ' .
'to OnlyTheseUsers'
);
}
/**
* Test permissions on duplicate page
* @dataProvider groupWithPermissions
*/
public function testDuplicatePageWithGroupPermissions(string $userName, string $method, bool $expected)
{
$originalPage = $this->objFromFixture(SiteTree::class, 'originalpage');
$user = $this->objFromFixture(Member::class, $userName);
$dupe = $originalPage->duplicate();
$this->assertEquals($originalPage->Title, $dupe->Title);
$this->assertEquals($dupe->CanViewType, 'OnlyTheseUsers');
$this->assertEquals($dupe->CanEditType, 'OnlyTheseUsers');
$this->assertSame($dupe->{$method}($user), $expected);
}
public function groupWithPermissions(): array
{
return [
'Subadmin can view page duplicate.' => [
'subadmin',
'canView',
true,
],
'Subadmin can edit page duplicate.' => [
'subadmin',
'canEdit',
true,
],
'Editor can view page duplicate.' => [
'editor',
'canView',
true,
],
'Editor can edit page duplicate.' => [
'editor',
'canEdit',
true,
],
'User with "allsections" permission can view page duplicate.' => [
'allsections',
'canView',
true,
],
'User with "allsections" permission cannot edit page duplicate.' => [
'allsections',
'canEdit',
false,
],
'Websiteuser permission cannot view page duplicate.' => [
'websiteuser',
'canView',
false,
],
'Websiteuser permission cannot edit page duplicate.' => [
'websiteuser',
'canEdit',
false,
],
];
$this->assertFalse($page->canEdit($user), 'Website user can\'t edit a page when set to inherit from the SiteConfig, and SiteConfig has canEdit set to OnlyTheseUsers');
}
}

View File

@ -11,8 +11,6 @@ SilverStripe\Security\Permission:
Code: CMS_ACCESS_CMSMain
grantaccess:
Code: SITETREE_GRANT_ACCESS
allsections:
Code: CMS_ACCESS_LeftAndMain
SilverStripe\Security\Group:
subadmingroup:
Title: Create, edit and delete pages
@ -22,9 +20,6 @@ SilverStripe\Security\Group:
Title: Edit existing pages
Code: editorgroup
Permissions: =>SilverStripe\Security\Permission.cmsmain2
allsectionsgroup:
Title: All Section Editors
Permissions: =>SilverStripe\Security\Permission.allsections
websiteusers:
Title: View certain restricted pages
SilverStripe\Security\Member:
@ -36,15 +31,11 @@ SilverStripe\Security\Member:
Email: editor@test.com
Password: test
Groups: =>SilverStripe\Security\Group.editorgroup
allsections:
Email: allsections@test.com
Password: test
Groups: =>SilverStripe\Security\Group.allsectionsgroup
websiteuser:
Email: websiteuser@test.com
Password: test
Groups: =>SilverStripe\Security\Group.websiteusers
SilverStripe\CMS\Model\SiteTree:
Page:
standardpage:
URLSegment: standardpage
restrictedViewLoggedInUsers:
@ -75,7 +66,7 @@ SilverStripe\CMS\Model\SiteTree:
URLSegment: parent-restrictedViewOnlySubadminGroup
child_restrictedViewOnlySubadminGroup:
CanViewType: Inherit
Parent: =>SilverStripe\CMS\Model\SiteTree.parent_restrictedViewOnlySubadminGroup
Parent: =>Page.parent_restrictedViewOnlySubadminGroup
URLSegment: child-restrictedViewOnlySubadminGroup
parent_restrictedEditOnlySubadminGroup:
CanEditType: OnlyTheseUsers
@ -83,7 +74,7 @@ SilverStripe\CMS\Model\SiteTree:
URLSegment: parent-restrictedEditOnlySubadminGroup
child_restrictedEditOnlySubadminGroup:
CanEditType: Inherit
Parent: =>SilverStripe\CMS\Model\SiteTree.parent_restrictedEditOnlySubadminGroup
Parent: =>Page.parent_restrictedEditOnlySubadminGroup
URLSegment: child-restrictedEditOnlySubadminGroup
deleteTestParentPage:
CanEditType: Inherit
@ -95,9 +86,3 @@ SilverStripe\CMS\Model\SiteTree:
draftOnlyPage:
CanViewType: Anyone
URLSegment: draft-only
originalpage:
Title: Original Page for duplicate
CanEditType: OnlyTheseUsers
CanViewType: OnlyTheseUsers
EditorGroups: =>SilverStripe\Security\Group.subadmingroup,=>SilverStripe\Security\Group.editorgroup
ViewerGroups: =>SilverStripe\Security\Group.subadmingroup,=>SilverStripe\Security\Group.editorgroup,=>SilverStripe\Security\Group.allsectionsgroup

View File

@ -3,6 +3,7 @@
namespace SilverStripe\CMS\Tests\Model;
use LogicException;
use Page;
use Psr\SimpleCache\CacheInterface;
use ReflectionMethod;
use SilverStripe\CMS\Model\RedirectorPage;
@ -33,15 +34,16 @@ use SilverStripe\Security\Security;
use SilverStripe\SiteConfig\SiteConfig;
use SilverStripe\Subsites\Extensions\SiteTreeSubsites;
use SilverStripe\Versioned\Versioned;
use SilverStripe\View\Parsers\HtmlDiff;
use SilverStripe\View\Parsers\Diff;
use SilverStripe\View\Parsers\ShortcodeParser;
use SilverStripe\View\Parsers\URLSegmentFilter;
use SilverStripe\View\Shortcodes\EmbedShortcodeProvider;
use TractorCow\Fluent\Extension\FluentSiteTreeExtension;
use Page;
use PageController;
use const RESOURCES_DIR;
use SilverStripe\Dev\Deprecation;
use SilverStripe\HTML5\HTML5Value;
use SilverStripe\View\Parsers\HTMLValue;
use SilverStripe\View\Parsers\HTML4Value;
class SiteTreeTest extends SapphireTest
{
@ -86,28 +88,28 @@ class SiteTreeTest extends SapphireTest
public function testCreateDefaultpages()
{
$remove = SiteTree::get();
$remove = SiteTree::get();
if ($remove) {
foreach ($remove as $page) {
$page->delete();
}
}
// Make sure the table is empty
$this->assertEquals(DB::query('SELECT COUNT("ID") FROM "SiteTree"')->value(), 0);
// Make sure the table is empty
$this->assertEquals(DB::query('SELECT COUNT("ID") FROM "SiteTree"')->value(), 0);
// Disable the creation
SiteTree::config()->create_default_pages = false;
singleton(SiteTree::class)->requireDefaultRecords();
// Disable the creation
SiteTree::config()->create_default_pages = false;
singleton(SiteTree::class)->requireDefaultRecords();
// The table should still be empty
$this->assertEquals(DB::query('SELECT COUNT("ID") FROM "SiteTree"')->value(), 0);
// The table should still be empty
$this->assertEquals(DB::query('SELECT COUNT("ID") FROM "SiteTree"')->value(), 0);
// Enable the creation
SiteTree::config()->create_default_pages = true;
singleton(SiteTree::class)->requireDefaultRecords();
// Enable the creation
SiteTree::config()->create_default_pages = true;
singleton(SiteTree::class)->requireDefaultRecords();
// The table should now have three rows (home, about-us, contact-us)
$this->assertEquals(DB::query('SELECT COUNT("ID") FROM "SiteTree"')->value(), 3);
// The table should now have three rows (home, about-us, contact-us)
$this->assertEquals(DB::query('SELECT COUNT("ID") FROM "SiteTree"')->value(), 3);
}
/**
@ -135,7 +137,7 @@ class SiteTreeTest extends SapphireTest
];
foreach ($expectedURLs as $fixture => $urlSegment) {
$obj = $this->objFromFixture(SiteTree::class, $fixture);
$obj = $this->objFromFixture('Page', $fixture);
$this->assertEquals($urlSegment, $obj->URLSegment);
}
}
@ -146,9 +148,9 @@ class SiteTreeTest extends SapphireTest
*/
public function testDisallowedURLGeneration($title, $urlSegment)
{
$page = new SiteTree(['Title' => $title]);
$page = Page::create(['Title' => $title]);
$id = $page->write();
$page = SiteTree::get()->byID($id);
$page = Page::get()->byID($id);
$this->assertEquals($urlSegment, $page->URLSegment);
}
@ -161,9 +163,9 @@ class SiteTreeTest extends SapphireTest
{
// Using the same dataprovider, strip out the -2 from the admin and dev segment
$urlSegment = str_replace('-2', '', $urlSegment ?? '');
$page = new SiteTree(['Title' => $title, 'ParentID' => 1]);
$page = Page::create(['Title' => $title, 'ParentID' => 1]);
$id = $page->write();
$page = SiteTree::get()->byID($id);
$page = Page::get()->byID($id);
$this->assertEquals($urlSegment, $page->URLSegment);
}
@ -178,7 +180,7 @@ class SiteTreeTest extends SapphireTest
$this->markTestSkipped('This legacy test requires RESOURCES_DIR to be "resources"');
}
$page = new SiteTree(['Title' => 'Resources']);
$page = SiteTree::create(['Title' => 'Resources']);
$id = $page->write();
$page = SiteTree::get()->byID($id);
$this->assertSame('resources-2', $page->URLSegment);
@ -196,7 +198,7 @@ class SiteTreeTest extends SapphireTest
$this->markTestSkipped('This test requires RESOURCES_DIR to be something other than "resources"');
}
$page = new SiteTree(['Title' => '_Resources']);
$page = SiteTree::create(['Title' => '_Resources']);
$id = $page->write();
$page = SiteTree::get()->byID($id);
$this->assertSame('resources', $page->URLSegment);
@ -207,12 +209,10 @@ class SiteTreeTest extends SapphireTest
*/
public function testPublishCopiesToLiveTable()
{
$obj = $this->objFromFixture(SiteTree::class, 'about');
$obj = $this->objFromFixture('Page', 'about');
$obj->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$createdID = DB::query(
"SELECT \"ID\" FROM \"SiteTree_Live\" WHERE \"URLSegment\" = '$obj->URLSegment'"
)->value();
$createdID = DB::query("SELECT \"ID\" FROM \"SiteTree_Live\" WHERE \"URLSegment\" = '$obj->URLSegment'")->value();
$this->assertEquals($obj->ID, $createdID);
}
@ -223,15 +223,12 @@ class SiteTreeTest extends SapphireTest
{
$this->logInWithPermission('ADMIN');
$obj = $this->objFromFixture(SiteTree::class, 'about');
$obj = $this->objFromFixture('Page', 'about');
$obj->Title = "asdfasdf";
$obj->write();
$this->assertTrue($obj->publishRecursive());
$this->assertEquals(
'asdfasdf',
DB::query("SELECT \"Title\" FROM \"SiteTree_Live\" WHERE \"ID\" = '$obj->ID'")->value()
);
$this->assertEquals('asdfasdf', DB::query("SELECT \"Title\" FROM \"SiteTree_Live\" WHERE \"ID\" = '$obj->ID'")->value());
$obj->Title = null;
$obj->write();
@ -278,7 +275,7 @@ class SiteTreeTest extends SapphireTest
Versioned::set_stage(Versioned::LIVE);
$checkSiteTree = DataObject::get_one(SiteTree::class, [
'"SiteTree"."URLSegment"' => 'get-one-test-page',
'"SiteTree"."URLSegment"' => 'get-one-test-page'
]);
$this->assertEquals("V1", $checkSiteTree->Title);
@ -317,13 +314,10 @@ class SiteTreeTest extends SapphireTest
{
/* DataObject::write() should save to a has_one relationship if you set a field called (relname)ID */
$page = new SiteTree();
$parentID = $this->idFromFixture(SiteTree::class, 'home');
$parentID = $this->idFromFixture('Page', 'home');
$page->ParentID = $parentID;
$page->write();
$this->assertEquals(
$parentID,
DB::query("SELECT \"ParentID\" FROM \"SiteTree\" WHERE \"ID\" = $page->ID")->value()
);
$this->assertEquals($parentID, DB::query("SELECT \"ParentID\" FROM \"SiteTree\" WHERE \"ID\" = $page->ID")->value());
/* You should then be able to save a null/0/'' value to the relation */
$page->ParentID = null;
@ -407,23 +401,23 @@ class SiteTreeTest extends SapphireTest
*/
public function testRestoreToStage()
{
$page = $this->objFromFixture(SiteTree::class, 'about');
$page = $this->objFromFixture('Page', 'about');
$pageID = $page->ID;
$page->delete();
$this->assertTrue(!DataObject::get_by_id(SiteTree::class, $pageID));
$this->assertTrue(!DataObject::get_by_id("Page", $pageID));
$deletedPage = Versioned::get_latest_version(SiteTree::class, $pageID);
$resultPage = $deletedPage->doRestoreToStage();
$requeriedPage = DataObject::get_by_id(SiteTree::class, $pageID);
$requeriedPage = DataObject::get_by_id("Page", $pageID);
$this->assertEquals($pageID, $resultPage->ID);
$this->assertEquals($pageID, $requeriedPage->ID);
$this->assertEquals('About Us', $requeriedPage->Title);
$this->assertInstanceOf(SiteTree::class, $requeriedPage);
$this->assertInstanceOf('Page', $requeriedPage);
$page2 = $this->objFromFixture(SiteTree::class, 'products');
$page2 = $this->objFromFixture('Page', 'products');
$page2ID = $page2->ID;
$page2->doUnpublish();
$page2->delete();
@ -433,14 +427,12 @@ class SiteTreeTest extends SapphireTest
Versioned::set_stage(Versioned::LIVE);
$deletedPage = Versioned::get_latest_version(SiteTree::class, $page2ID);
$deletedPage->doRestoreToStage();
$this->assertFalse(
(bool)Versioned::get_one_by_stage(SiteTree::class, Versioned::LIVE, "\"SiteTree\".\"ID\" = " . $page2ID)
);
$this->assertFalse((bool)Versioned::get_one_by_stage(SiteTree::class, Versioned::LIVE, "\"SiteTree\".\"ID\" = " . $page2ID));
Versioned::set_stage(Versioned::DRAFT);
$requeriedPage = DataObject::get_by_id(SiteTree::class, $page2ID);
$requeriedPage = DataObject::get_by_id("Page", $page2ID);
$this->assertEquals('Products', $requeriedPage->Title);
$this->assertInstanceOf(SiteTree::class, $requeriedPage);
$this->assertInstanceOf('Page', $requeriedPage);
}
public function testNoCascadingDeleteWithoutID()
@ -464,11 +456,11 @@ class SiteTreeTest extends SapphireTest
public function testGetByLink()
{
$home = $this->objFromFixture(SiteTree::class, 'home');
$about = $this->objFromFixture(SiteTree::class, 'about');
$staff = $this->objFromFixture(SiteTree::class, 'staff');
$product = $this->objFromFixture(SiteTree::class, 'product1');
$numeric0 = $this->objFromFixture(SiteTree::class, 'numeric0');
$home = $this->objFromFixture('Page', 'home');
$about = $this->objFromFixture('Page', 'about');
$staff = $this->objFromFixture('Page', 'staff');
$product = $this->objFromFixture('Page', 'product1');
$numeric0 = $this->objFromFixture('Page', 'numeric0');
SiteTree::config()->nested_urls = false;
@ -497,11 +489,11 @@ class SiteTreeTest extends SapphireTest
public function testGetByLinkAbsolute()
{
$home = $this->objFromFixture(SiteTree::class, 'home');
$about = $this->objFromFixture(SiteTree::class, 'about');
$staff = $this->objFromFixture(SiteTree::class, 'staff');
$product = $this->objFromFixture(SiteTree::class, 'product1');
$numeric0 = $this->objFromFixture(SiteTree::class, 'numeric0');
$home = $this->objFromFixture('Page', 'home');
$about = $this->objFromFixture('Page', 'about');
$staff = $this->objFromFixture('Page', 'staff');
$product = $this->objFromFixture('Page', 'product1');
$numeric0 = $this->objFromFixture('Page', 'numeric0');
$base = 'https://example.test/';
$this->assertEquals($home->ID, SiteTree::get_by_link(Controller::join_links($base, '/'), false)->ID);
@ -514,51 +506,31 @@ class SiteTreeTest extends SapphireTest
public function testRelativeLink()
{
$about = $this->objFromFixture(SiteTree::class, 'about');
$staff = $this->objFromFixture(SiteTree::class, 'staff');
$numeric0 = $this->objFromFixture(SiteTree::class, 'numeric0');
$about = $this->objFromFixture('Page', 'about');
$staff = $this->objFromFixture('Page', 'staff');
$numeric0 = $this->objFromFixture('Page', 'numeric0');
Config::modify()->set(SiteTree::class, 'nested_urls', true);
$this->assertEquals(
'about-us',
$about->RelativeLink(),
'Matches URLSegment on top level without parameters'
);
$this->assertEquals(
'about-us/my-staff',
$staff->RelativeLink(),
'Matches URLSegment plus parent on second level without parameters'
);
$this->assertEquals(
Controller::normaliseTrailingSlash('0/'),
$numeric0->RelativeLink(),
'Matches URLSegment for segment = 0'
);
$this->assertEquals(
'about-us/edit',
$about->RelativeLink('edit'),
'Matches URLSegment plus parameter on top level'
);
$this->assertEquals(
'about-us/tom&jerry',
$about->RelativeLink('tom&jerry'),
'Doesnt url encode parameter'
);
$this->assertEquals('about-us/', $about->RelativeLink(), 'Matches URLSegment on top level without parameters');
$this->assertEquals('about-us/my-staff/', $staff->RelativeLink(), 'Matches URLSegment plus parent on second level without parameters');
$this->assertEquals('about-us/edit', $about->RelativeLink('edit'), 'Matches URLSegment plus parameter on top level');
$this->assertEquals('about-us/tom&jerry', $about->RelativeLink('tom&jerry'), 'Doesnt url encode parameter');
$this->assertEquals('0/', $numeric0->RelativeLink(), 'Matches URLSegment for segment = 0');
}
public function testPageLevel()
{
$about = $this->objFromFixture(SiteTree::class, 'about');
$staff = $this->objFromFixture(SiteTree::class, 'staff');
$about = $this->objFromFixture('Page', 'about');
$staff = $this->objFromFixture('Page', 'staff');
$this->assertEquals(1, $about->getPageLevel());
$this->assertEquals(2, $staff->getPageLevel());
}
public function testAbsoluteLiveLink()
{
$parent = $this->objFromFixture(SiteTree::class, 'about');
$child = $this->objFromFixture(SiteTree::class, 'staff');
$parent = $this->objFromFixture('Page', 'about');
$child = $this->objFromFixture('Page', 'staff');
Config::modify()->set(SiteTree::class, 'nested_urls', true);
@ -569,23 +541,23 @@ class SiteTreeTest extends SapphireTest
$parent->URLSegment = 'changed-on-draft';
$parent->write();
$this->assertStringEndsWith('changed-on-live/my-staff', $child->getAbsoluteLiveLink(false));
$this->assertStringEndsWith('changed-on-live/my-staff?stage=Live', $child->getAbsoluteLiveLink());
$this->assertStringEndsWith('changed-on-live/my-staff/', $child->getAbsoluteLiveLink(false));
$this->assertStringEndsWith('changed-on-live/my-staff/?stage=Live', $child->getAbsoluteLiveLink());
}
public function testDuplicateChildrenRetainSort()
{
$parent = new SiteTree();
$parent = new Page();
$parent->Title = 'Parent';
$parent->write();
$child1 = new SiteTree();
$child1 = new Page();
$child1->ParentID = $parent->ID;
$child1->Title = 'Child 1';
$child1->Sort = 2;
$child1->write();
$child2 = new SiteTree();
$child2 = new Page();
$child2->ParentID = $parent->ID;
$child2->Title = 'Child 2';
$child2->Sort = 1;
@ -609,34 +581,34 @@ class SiteTreeTest extends SapphireTest
public function testDeleteFromStageOperatesRecursively()
{
Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', false);
$pageAbout = $this->objFromFixture(SiteTree::class, 'about');
$pageStaff = $this->objFromFixture(SiteTree::class, 'staff');
$pageStaffDuplicate = $this->objFromFixture(SiteTree::class, 'staffduplicate');
$pageAbout = $this->objFromFixture('Page', 'about');
$pageStaff = $this->objFromFixture('Page', 'staff');
$pageStaffDuplicate = $this->objFromFixture('Page', 'staffduplicate');
$pageAbout->delete();
$this->assertNull(DataObject::get_by_id(SiteTree::class, $pageAbout->ID));
$this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaff->ID) instanceof SiteTree);
$this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaffDuplicate->ID) instanceof SiteTree);
$this->assertNull(DataObject::get_by_id('Page', $pageAbout->ID));
$this->assertTrue(DataObject::get_by_id('Page', $pageStaff->ID) instanceof Page);
$this->assertTrue(DataObject::get_by_id('Page', $pageStaffDuplicate->ID) instanceof Page);
Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', true);
}
public function testDeleteFromStageOperatesRecursivelyStrict()
{
$pageAbout = $this->objFromFixture(SiteTree::class, 'about');
$pageStaff = $this->objFromFixture(SiteTree::class, 'staff');
$pageStaffDuplicate = $this->objFromFixture(SiteTree::class, 'staffduplicate');
$pageAbout = $this->objFromFixture('Page', 'about');
$pageStaff = $this->objFromFixture('Page', 'staff');
$pageStaffDuplicate = $this->objFromFixture('Page', 'staffduplicate');
$pageAbout->delete();
$this->assertNull(DataObject::get_by_id(SiteTree::class, $pageAbout->ID));
$this->assertNull(DataObject::get_by_id(SiteTree::class, $pageStaff->ID));
$this->assertNull(DataObject::get_by_id(SiteTree::class, $pageStaffDuplicate->ID));
$this->assertNull(DataObject::get_by_id('Page', $pageAbout->ID));
$this->assertNull(DataObject::get_by_id('Page', $pageStaff->ID));
$this->assertNull(DataObject::get_by_id('Page', $pageStaffDuplicate->ID));
}
public function testDuplicate()
{
$pageAbout = $this->objFromFixture(SiteTree::class, 'about');
$pageAbout = $this->objFromFixture('Page', 'about');
$dupe = $pageAbout->duplicate();
$this->assertEquals($pageAbout->Title, $dupe->Title);
$this->assertNotEquals($pageAbout->URLSegment, $dupe->URLSegment);
@ -648,22 +620,22 @@ class SiteTreeTest extends SapphireTest
Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', false);
$this->logInWithPermission('ADMIN');
$pageAbout = $this->objFromFixture(SiteTree::class, 'about');
$pageAbout = $this->objFromFixture('Page', 'about');
$pageAbout->publishRecursive();
$pageStaff = $this->objFromFixture(SiteTree::class, 'staff');
$pageStaff = $this->objFromFixture('Page', 'staff');
$pageStaff->publishRecursive();
$pageStaffDuplicate = $this->objFromFixture(SiteTree::class, 'staffduplicate');
$pageStaffDuplicate = $this->objFromFixture('Page', 'staffduplicate');
$pageStaffDuplicate->publishRecursive();
$parentPage = $this->objFromFixture(SiteTree::class, 'about');
$parentPage = $this->objFromFixture('Page', 'about');
$parentPage->doUnpublish();
Versioned::set_stage(Versioned::LIVE);
$this->assertNull(DataObject::get_by_id(SiteTree::class, $pageAbout->ID));
$this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaff->ID) instanceof SiteTree);
$this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaffDuplicate->ID) instanceof SiteTree);
$this->assertNull(DataObject::get_by_id('Page', $pageAbout->ID));
$this->assertTrue(DataObject::get_by_id('Page', $pageStaff->ID) instanceof Page);
$this->assertTrue(DataObject::get_by_id('Page', $pageStaffDuplicate->ID) instanceof Page);
Versioned::set_stage(Versioned::DRAFT);
Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', true);
}
@ -673,20 +645,20 @@ class SiteTreeTest extends SapphireTest
Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', false);
$this->logInWithPermission('ADMIN');
$pageAbout = $this->objFromFixture(SiteTree::class, 'about');
$pageAbout = $this->objFromFixture('Page', 'about');
$pageAbout->publishRecursive();
$pageStaff = $this->objFromFixture(SiteTree::class, 'staff');
$pageStaff = $this->objFromFixture('Page', 'staff');
$pageStaff->publishRecursive();
$pageStaffDuplicate = $this->objFromFixture(SiteTree::class, 'staffduplicate');
$pageStaffDuplicate = $this->objFromFixture('Page', 'staffduplicate');
$pageStaffDuplicate->publishRecursive();
$parentPage = $this->objFromFixture(SiteTree::class, 'about');
$parentPage = $this->objFromFixture('Page', 'about');
$parentPage->doUnpublish();
Versioned::set_stage(Versioned::LIVE);
$this->assertNull(DataObject::get_by_id(SiteTree::class, $pageAbout->ID));
$this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaff->ID) instanceof SiteTree);
$this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaffDuplicate->ID) instanceof SiteTree);
$this->assertNull(DataObject::get_by_id('Page', $pageAbout->ID));
$this->assertTrue(DataObject::get_by_id('Page', $pageStaff->ID) instanceof Page);
$this->assertTrue(DataObject::get_by_id('Page', $pageStaffDuplicate->ID) instanceof Page);
Versioned::set_stage(Versioned::DRAFT);
Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', true);
}
@ -695,20 +667,20 @@ class SiteTreeTest extends SapphireTest
{
$this->logInWithPermission('ADMIN');
$pageAbout = $this->objFromFixture(SiteTree::class, 'about');
$pageAbout = $this->objFromFixture('Page', 'about');
$pageAbout->publishRecursive();
$pageStaff = $this->objFromFixture(SiteTree::class, 'staff');
$pageStaff = $this->objFromFixture('Page', 'staff');
$pageStaff->publishRecursive();
$pageStaffDuplicate = $this->objFromFixture(SiteTree::class, 'staffduplicate');
$pageStaffDuplicate = $this->objFromFixture('Page', 'staffduplicate');
$pageStaffDuplicate->publishRecursive();
$parentPage = $this->objFromFixture(SiteTree::class, 'about');
$parentPage = $this->objFromFixture('Page', 'about');
$parentPage->doUnpublish();
Versioned::set_stage(Versioned::LIVE);
$this->assertNull(DataObject::get_by_id(SiteTree::class, $pageAbout->ID));
$this->assertNull(DataObject::get_by_id(SiteTree::class, $pageStaff->ID));
$this->assertNull(DataObject::get_by_id(SiteTree::class, $pageStaffDuplicate->ID));
$this->assertNull(DataObject::get_by_id('Page', $pageAbout->ID));
$this->assertNull(DataObject::get_by_id('Page', $pageStaff->ID));
$this->assertNull(DataObject::get_by_id('Page', $pageStaffDuplicate->ID));
Versioned::set_stage(Versioned::DRAFT);
}
@ -719,7 +691,7 @@ class SiteTreeTest extends SapphireTest
public function testReadArchiveDate()
{
DBDatetime::set_mock_now('2009-07-02 14:05:07');
$oldPage = new SiteTree();
$oldPage = SiteTree::create();
$oldPage->Title = 'A really old page';
$oldPage->write();
DBDatetime::clear_mock_now();
@ -727,7 +699,7 @@ class SiteTreeTest extends SapphireTest
$date = '2009-07-02 14:05:07';
Versioned::reading_archived_date($date);
$result = SiteTree::get()->where([
'"SiteTree"."ParentID"' => 0,
'"SiteTree"."ParentID"' => 0
]);
$this->assertCount(1, $result, '"A really old page" should be returned');
}
@ -736,11 +708,11 @@ class SiteTreeTest extends SapphireTest
{
$editor = $this->objFromFixture(Member::class, "editor");
$home = $this->objFromFixture(SiteTree::class, "home");
$staff = $this->objFromFixture(SiteTree::class, "staff");
$products = $this->objFromFixture(SiteTree::class, "products");
$product1 = $this->objFromFixture(SiteTree::class, "product1");
$product4 = $this->objFromFixture(SiteTree::class, "product4");
$home = $this->objFromFixture("Page", "home");
$staff = $this->objFromFixture("Page", "staff");
$products = $this->objFromFixture("Page", "products");
$product1 = $this->objFromFixture("Page", "product1");
$product4 = $this->objFromFixture("Page", "product4");
// Test logged out users cannot edit
$this->logOut();
@ -761,7 +733,7 @@ class SiteTreeTest extends SapphireTest
public function testCanEditWithAccessToAllSections()
{
$page = new SiteTree();
$page = new Page();
$page->write();
$allSectionMember = $this->objFromFixture(Member::class, 'allsections');
$securityAdminMember = $this->objFromFixture(Member::class, 'securityadmin');
@ -786,7 +758,7 @@ class SiteTreeTest extends SapphireTest
$this->assertTrue(singleton(SiteTree::class)->canCreate());
// Test creation underneath a parent which this user doesn't have access to
$parent = $this->objFromFixture(SiteTree::class, 'about');
$parent = $this->objFromFixture('Page', 'about');
$this->assertFalse(singleton(SiteTree::class)->canCreate(null, ['Parent' => $parent]));
// Test creation underneath a parent which doesn't allow a certain child
@ -802,18 +774,13 @@ class SiteTreeTest extends SapphireTest
$this->assertTrue(singleton(SiteTree::class)->canCreate(null, ['Parent' => singleton(SiteTree::class)]));
//Test we don't check for allowedChildren on parent context if it's not SiteTree instance
$this->assertTrue(
singleton(SiteTree::class)->canCreate(
null,
['Parent' => $this->objFromFixture(SiteTreeTest_DataObject::class, 'relations')]
)
);
$this->assertTrue(singleton(SiteTree::class)->canCreate(null, ['Parent' => $this->objFromFixture(SiteTreeTest_DataObject::class, 'relations')]));
}
public function testEditPermissionsOnDraftVsLive()
{
// Create an inherit-permission page
$page = new SiteTree();
$page = new Page();
$page->write();
$page->CanEditType = "Inherit";
$page->publishRecursive();
@ -861,10 +828,10 @@ class SiteTreeTest extends SapphireTest
public function testCompareVersions()
{
// Necessary to avoid
$oldCleanerClass = HtmlDiff::$html_cleaner_class;
HtmlDiff::$html_cleaner_class = SiteTreeTest_NullHtmlCleaner::class;
$oldCleanerClass = Diff::$html_cleaner_class;
Diff::$html_cleaner_class = SiteTreeTest_NullHtmlCleaner::class;
$page = new SiteTree();
$page = new Page();
$page->write();
$this->assertEquals(1, $page->Version);
@ -882,7 +849,7 @@ class SiteTreeTest extends SapphireTest
$processedContent = preg_replace('/>\s*/', '>', $processedContent ?? '');
$this->assertEquals("<ins><span>This is a test</span></ins>", $processedContent);
HtmlDiff::$html_cleaner_class = $oldCleanerClass;
Diff::$html_cleaner_class = $oldCleanerClass;
}
public function testAuthorIDAndPublisherIDFilledOutOnPublish()
@ -892,16 +859,16 @@ class SiteTreeTest extends SapphireTest
$this->logInAs($member);
// Write the page
$about = $this->objFromFixture(SiteTree::class, 'about');
$about = $this->objFromFixture('Page', 'about');
$about->Title = "Another title";
$about->write();
// Check the version created
$savedVersion = DB::prepared_query(
"SELECT \"AuthorID\", \"PublisherID\" FROM \"SiteTree_Versions\"
WHERE \"RecordID\" = ? ORDER BY \"Version\" DESC",
WHERE \"RecordID\" = ? ORDER BY \"Version\" DESC",
[$about->ID]
)->record();
)->first();
$this->assertEquals($member->ID, $savedVersion['AuthorID']);
$this->assertEquals(0, $savedVersion['PublisherID']);
@ -909,9 +876,9 @@ class SiteTreeTest extends SapphireTest
$about->publishRecursive();
$publishedVersion = DB::prepared_query(
"SELECT \"AuthorID\", \"PublisherID\" FROM \"SiteTree_Versions\"
WHERE \"RecordID\" = ? ORDER BY \"Version\" DESC",
WHERE \"RecordID\" = ? ORDER BY \"Version\" DESC",
[$about->ID]
)->record();
)->first();
// Check the version created
$this->assertEquals($member->ID, $publishedVersion['AuthorID']);
@ -920,7 +887,7 @@ class SiteTreeTest extends SapphireTest
public function testLinkShortcodeHandler()
{
$aboutPage = $this->objFromFixture(SiteTree::class, 'about');
$aboutPage = $this->objFromFixture('Page', 'about');
$redirectPage = $this->objFromFixture(RedirectorPage::class, 'external');
$parser = new ShortcodeParser();
@ -932,24 +899,12 @@ class SiteTreeTest extends SapphireTest
$aboutShortcodeExpected = $aboutPage->Link();
$aboutEnclosedExpected = sprintf('<a href="%s">Example Content</a>', $aboutPage->Link());
$this->assertEquals(
$aboutShortcodeExpected,
$parser->parse($aboutShortcode),
'Test that simple linking works.'
);
$this->assertEquals(
$aboutEnclosedExpected,
$parser->parse($aboutEnclosed),
'Test enclosed content is linked.'
);
$this->assertEquals($aboutShortcodeExpected, $parser->parse($aboutShortcode), 'Test that simple linking works.');
$this->assertEquals($aboutEnclosedExpected, $parser->parse($aboutEnclosed), 'Test enclosed content is linked.');
$aboutPage->delete();
$this->assertEquals(
$aboutShortcodeExpected,
$parser->parse($aboutShortcode),
'Test that deleted pages still link.'
);
$this->assertEquals($aboutShortcodeExpected, $parser->parse($aboutShortcode), 'Test that deleted pages still link.');
$this->assertEquals($aboutEnclosedExpected, $parser->parse($aboutEnclosed));
$aboutShortcode = '[sitetree_link,id="-1"]';
@ -963,10 +918,7 @@ class SiteTreeTest extends SapphireTest
$redirectExpected = 'http://www.google.com?a&amp;b';
$this->assertEquals($redirectExpected, $parser->parse($redirectShortcode));
$this->assertEquals(
sprintf('<a href="%s">Example Content</a>', $redirectExpected),
$parser->parse($redirectEnclosed)
);
$this->assertEquals(sprintf('<a href="%s">Example Content</a>', $redirectExpected), $parser->parse($redirectEnclosed));
$this->assertEquals('', $parser->parse('[sitetree_link]'), 'Test that invalid ID attributes are not parsed.');
$this->assertEquals('', $parser->parse('[sitetree_link,id="text"]'));
@ -975,8 +927,8 @@ class SiteTreeTest extends SapphireTest
public function testIsCurrent()
{
$aboutPage = $this->objFromFixture(SiteTree::class, 'about');
$productPage = $this->objFromFixture(SiteTree::class, 'products');
$aboutPage = $this->objFromFixture('Page', 'about');
$productPage = $this->objFromFixture('Page', 'products');
Director::set_current_page($aboutPage);
$this->assertTrue($aboutPage->isCurrent(), 'Assert that basic isCurrent checks works.');
@ -984,7 +936,7 @@ class SiteTreeTest extends SapphireTest
$this->assertTrue(
DataObject::get_one(SiteTree::class, [
'"SiteTree"."Title"' => 'About Us',
'"SiteTree"."Title"' => 'About Us'
])->isCurrent(),
'Assert that isCurrent works on another instance with the same ID.'
);
@ -995,9 +947,9 @@ class SiteTreeTest extends SapphireTest
public function testIsSection()
{
$about = $this->objFromFixture(SiteTree::class, 'about');
$staff = $this->objFromFixture(SiteTree::class, 'staff');
$ceo = $this->objFromFixture(SiteTree::class, 'ceo');
$about = $this->objFromFixture('Page', 'about');
$staff = $this->objFromFixture('Page', 'staff');
$ceo = $this->objFromFixture('Page', 'ceo');
Director::set_current_page($about);
$this->assertTrue($about->isSection());
@ -1017,7 +969,7 @@ class SiteTreeTest extends SapphireTest
public function testURLSegmentReserved()
{
$siteTree = new SiteTree(['URLSegment' => 'admin']);
$siteTree = SiteTree::create(['URLSegment' => 'admin']);
$segment = $siteTree->validURLSegment();
$this->assertFalse($segment);
@ -1105,7 +1057,7 @@ class SiteTreeTest extends SapphireTest
$sitetree->URLSegment = 'home-noconflict';
$this->assertTrue($sitetree->validURLSegment());
$sitetree->ParentID = $this->idFromFixture(SiteTree::class, 'about');
$sitetree->ParentID = $this->idFromFixture('Page', 'about');
$sitetree->URLSegment = 'home';
$this->assertFalse($sitetree->validURLSegment(), 'Conflicts are still recognised with a ParentID value');
@ -1115,7 +1067,7 @@ class SiteTreeTest extends SapphireTest
$sitetree->URLSegment = 'home';
$this->assertFalse($sitetree->validURLSegment(), 'URLSegment conflicts are recognised');
$sitetree->ParentID = $this->idFromFixture(SiteTree::class, 'about');
$sitetree->ParentID = $this->idFromFixture('Page', 'about');
$this->assertTrue($sitetree->validURLSegment(), 'URLSegments can be the same across levels');
$sitetree->URLSegment = 'my-staff';
@ -1185,18 +1137,13 @@ class SiteTreeTest extends SapphireTest
$sitetree->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$sitetree = DataObject::get_by_id(SiteTree::class, $sitetree->ID, false);
$this->assertEquals($sitetree->URLSegment, rawurlencode('brötchen'));
$sitetreeLive = Versioned::get_one_by_stage(
SiteTree::class,
Versioned::LIVE,
'"SiteTree"."ID" = ' . $sitetree->ID,
false
);
$sitetreeLive = Versioned::get_one_by_stage(SiteTree::class, Versioned::LIVE, '"SiteTree"."ID" = ' .$sitetree->ID, false);
$this->assertEquals($sitetreeLive->URLSegment, rawurlencode('brötchen'));
}
public function testVersionsAreCreated()
{
$p = new SiteTree();
$p = new Page();
$p->Content = "one";
$p->write();
$this->assertEquals(1, $p->Version);
@ -1221,13 +1168,6 @@ class SiteTreeTest extends SapphireTest
$this->assertEquals(3, $p->Version);
}
public function testHidePagetypes()
{
SiteTree::config()->set('hide_pagetypes', ['Page']);
$classes = SiteTree::page_type_classes();
$this->assertNotContains('Page', $classes);
}
public function testPageTypeClasses()
{
$classes = SiteTree::page_type_classes();
@ -1242,11 +1182,6 @@ class SiteTreeTest extends SapphireTest
$newClasses,
'Setting hide_ancestor to a boolean (incorrect) value caused a page class to be hidden'
);
// Testing what happens if a valid config value is set
Config::modify()->set(SiteTreeTest_ClassA::class, 'hide_ancestor', 'Page');
$classes = SiteTree::page_type_classes();
$this->assertNotContains('Page', $classes);
}
/**
@ -1296,23 +1231,23 @@ class SiteTreeTest extends SapphireTest
// Expected
[ SiteTreeTest_ClassB::class ],
// Assertion message
'Direct setting of allowed children',
'Direct setting of allowed children'
],
[
SiteTreeTest_ClassB::class,
[ SiteTreeTest_ClassC::class, SiteTreeTest_ClassCext::class ],
'Includes subclasses',
'Includes subclasses'
],
[
SiteTreeTest_ClassC::class,
[],
'Null setting',
'Null setting'
],
[
SiteTreeTest_ClassD::class,
[SiteTreeTest_ClassC::class],
'Excludes subclasses if class is prefixed by an asterisk',
],
'Excludes subclasses if class is prefixed by an asterisk'
]
];
}
@ -1353,10 +1288,7 @@ class SiteTreeTest extends SapphireTest
$classCext->ParentID = $classD->ID;
$valid = $classCext->validate();
$this->assertFalse(
$valid->isValid(),
"Doesnt allow child where only parent class is allowed on parent node, and asterisk prefixing is used"
);
$this->assertFalse($valid->isValid(), "Doesnt allow child where only parent class is allowed on parent node, and asterisk prefixing is used");
}
public function testClassDropdown()
@ -1375,8 +1307,8 @@ class SiteTreeTest extends SapphireTest
$this->assertArrayHasKey(SiteTreeTest_ClassA::class, $method->invoke($sitetree));
$this->logInWithPermission('ADMIN');
$rootPage = $this->objFromFixture(SiteTree::class, 'home');
$nonRootPage = $this->objFromFixture(SiteTree::class, 'staff');
$rootPage = $this->objFromFixture(Page::class, 'home');
$nonRootPage = $this->objFromFixture(Page::class, 'staff');
$this->assertArrayNotHasKey(SiteTreeTest_NotRoot::class, $method->invoke($rootPage));
$this->assertArrayHasKey(SiteTreeTest_NotRoot::class, $method->invoke($nonRootPage));
@ -1472,11 +1404,11 @@ class SiteTreeTest extends SapphireTest
public function testGetBreadcrumbItems()
{
$page = $this->objFromFixture(SiteTree::class, "breadcrumbs");
$page = $this->objFromFixture("Page", "breadcrumbs");
$this->assertEquals(1, $page->getBreadcrumbItems()->count(), "Only display current page.");
// Test breadcrumb order
$page = $this->objFromFixture(SiteTree::class, "breadcrumbs5");
$page = $this->objFromFixture("Page", "breadcrumbs5");
$breadcrumbs = $page->getBreadcrumbItems();
$this->assertEquals($breadcrumbs->count(), 5, "Display all breadcrumbs");
$this->assertEquals($breadcrumbs->first()->Title, "Breadcrumbs", "Breadcrumbs should be the first item.");
@ -1496,16 +1428,16 @@ class SiteTreeTest extends SapphireTest
public function testMetaTags()
{
$this->logInWithPermission('ADMIN');
$page = $this->objFromFixture(SiteTree::class, 'metapage');
$page = $this->objFromFixture('Page', 'metapage');
// Test with title
$meta = $page->MetaTags();
$charset = Config::inst()->get(ContentNegotiator::class, 'encoding');
$this->assertStringContainsString('<meta http-equiv="Content-Type" content="text/html; charset=' . $charset . '"', $meta);
$this->assertStringContainsString('<meta http-equiv="Content-Type" content="text/html; charset='.$charset.'"', $meta);
$this->assertStringContainsString('<meta name="description" content="The &lt;br /&gt; and &lt;br&gt; tags"', $meta);
$this->assertStringContainsString('<link rel="canonical" href="http://www.mysite.com/html-and-xml"', $meta);
$this->assertStringContainsString('<meta name="x-page-id" content="' . $page->ID.'"', $meta);
$this->assertStringContainsString('<meta name="x-cms-edit-link" content="' . $page->CMSEditLink().'"', $meta);
$this->assertStringContainsString('<meta name="x-page-id" content="'.$page->ID.'"', $meta);
$this->assertStringContainsString('<meta name="x-cms-edit-link" content="'.$page->CMSEditLink().'"', $meta);
$this->assertStringContainsString('<title>HTML &amp; XML</title>', $meta);
// Test without title
@ -1520,7 +1452,7 @@ class SiteTreeTest extends SapphireTest
{
$this->logInWithPermission('ADMIN');
/** @var SiteTree $page */
$page = $this->objFromFixture(SiteTree::class, 'metapage');
$page = $this->objFromFixture('Page', 'metapage');
$charset = Config::inst()->get(ContentNegotiator::class, 'encoding');
@ -1602,8 +1534,8 @@ class SiteTreeTest extends SapphireTest
// both pages are viewable in stage
Versioned::set_stage(Versioned::DRAFT);
$about = $this->objFromFixture(SiteTree::class, 'about');
$staff = $this->objFromFixture(SiteTree::class, 'staff');
$about = $this->objFromFixture('Page', 'about');
$staff = $this->objFromFixture('Page', 'staff');
$this->assertFalse($about->isOrphaned());
$this->assertFalse($staff->isOrphaned());
$this->assertTrue($about->canView($member));
@ -1614,23 +1546,23 @@ class SiteTreeTest extends SapphireTest
$this->assertFalse($staff->isOrphaned());
$this->assertTrue($staff->canView($member));
Versioned::set_stage(Versioned::LIVE);
$staff = $this->objFromFixture(SiteTree::class, 'staff'); // Live copy of page
$staff = $this->objFromFixture('Page', 'staff'); // Live copy of page
$this->assertTrue($staff->isOrphaned()); // because parent isn't published
$this->assertFalse($staff->canView($member));
// Publishing the parent page should restore visibility
Versioned::set_stage(Versioned::DRAFT);
$about = $this->objFromFixture(SiteTree::class, 'about');
$about = $this->objFromFixture('Page', 'about');
$about->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
Versioned::set_stage(Versioned::LIVE);
$staff = $this->objFromFixture(SiteTree::class, 'staff');
$staff = $this->objFromFixture('Page', 'staff');
$this->assertFalse($staff->isOrphaned());
$this->assertTrue($staff->canView($member));
// Removing staging page should not prevent live page being visible
$about->deleteFromStage('Stage');
$staff->deleteFromStage('Stage');
$staff = $this->objFromFixture(SiteTree::class, 'staff');
$staff = $this->objFromFixture('Page', 'staff');
$this->assertFalse($staff->isOrphaned());
$this->assertTrue($staff->canView($member));
@ -1645,8 +1577,8 @@ class SiteTreeTest extends SapphireTest
{
$this->logInWithPermission('ADMIN');
/** @var SiteTree $page */
$page = $this->objFromFixture(SiteTree::class, 'home');
/** @var Page $page */
$page = $this->objFromFixture('Page', 'home');
$this->assertTrue($page->canAddChildren());
$this->assertTrue($page->isOnDraft());
$this->assertFalse($page->isPublished());
@ -1723,24 +1655,23 @@ class SiteTreeTest extends SapphireTest
}
/**
* Test that the controller name for a Page instance can be gathered by appending "Controller" to the Page
* Test that the controller name for a SiteTree instance can be gathered by appending "Controller" to the SiteTree
* class name in a PSR-2 compliant manner.
*/
public function testGetControllerName()
{
$page = new Page();
$this->assertSame(PageController::class, $page->getControllerName());
$class = new Page;
$this->assertSame('PageController', $class->getControllerName());
}
/**
* Test that the controller name for a SiteTree instance can be gathered when set directly via config var
*/
public function testGetControllerNameFromConfig()
{
Config::inst()->set(SiteTree::class, 'controller_name', 'This\\Is\\A\\New\\Controller');
$page = new SiteTree();
$this->assertSame('This\\Is\\A\\New\\Controller', $page->getControllerName());
Config::inst()->set(Page::class, 'controller_name', 'This\\Is\\A\\New\\Controller');
$class = new Page;
$this->assertSame('This\\Is\\A\\New\\Controller', $class->getControllerName());
}
/**
@ -1756,9 +1687,21 @@ class SiteTreeTest extends SapphireTest
$this->assertSame(SiteTreeTest_NamespaceMapTestController::class, $namespacedSiteTree->getControllerName());
}
/**
* Test that underscored class names (legacy) are still supported (deprecation notice is issued though).
*/
public function testGetControllerNameWithUnderscoresIsSupported()
{
if (Deprecation::isEnabled()) {
$this->markTestSkipped('Test calls deprecated code');
}
$class = new SiteTreeTest_LegacyControllerName;
$this->assertEquals(SiteTreeTest_LegacyControllerName_Controller::class, $class->getControllerName());
}
public function testTreeTitleCache()
{
$siteTree = new SiteTree();
$siteTree = SiteTree::create();
$user = $this->objFromFixture(Member::class, 'allsections');
Security::setCurrentUser($user);
$pageClass = array_values(SiteTree::page_type_classes())[0];
@ -1845,7 +1788,7 @@ class SiteTreeTest extends SapphireTest
}
// Create new page on DRAFT
$page = new SiteTree();
$page = SiteTree::create();
$page->Content = $content;
$page->write();
@ -1861,7 +1804,7 @@ class SiteTreeTest extends SapphireTest
public function testGetCMSActions()
{
// Create new page on DRAFT
$page = new SiteTree();
$page = SiteTree::create();
$page->Content = md5(rand(0, PHP_INT_MAX));
$page->write();
@ -1991,7 +1934,7 @@ class SiteTreeTest extends SapphireTest
public function testGetCMSActionsWithoutForms()
{
// Create new page on DRAFT
$page = new SiteTree();
$page = SiteTree::create();
$page->Content = md5(rand(0, PHP_INT_MAX));
$page->write();
@ -2097,10 +2040,20 @@ class SiteTreeTest extends SapphireTest
*/
public function testSanitiseExtraMeta(string $extraMeta, string $expected, string $message): void
{
// If using HTML5Value then the 'somethingdodgy' test won't be converted to valid html
// However if using the default HTMLValue, then it will be converted to valid html
$isDodgyAndUsingHTML5 = strpos($expected, 'somethingdodgy') !== false &&
(HTMLValue::create() instanceof HTML5Value);
if ($isDodgyAndUsingHTML5) {
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('Custom Meta Tags does not contain valid HTML');
}
$siteTree = new SiteTree();
$siteTree->ExtraMeta = $extraMeta;
$siteTree->write();
$this->assertSame($expected, $siteTree->ExtraMeta, $message);
if (!$isDodgyAndUsingHTML5) {
$this->assertSame($expected, $siteTree->ExtraMeta, $message);
}
}
public function provideSanitiseExtraMeta(): array
@ -2130,12 +2083,50 @@ class SiteTreeTest extends SapphireTest
'<link rel="canonical" accesskey="X" onclick="alert(1)" name="x" />',
'<link rel="canonical" name="x">',
'Multiple attributes are removed'
],
]
];
}
/**
* @dataProvider provideSanatiseInvalidExtraMeta
*/
public function testSanatiseInvalidExtraMetaHTML4Value(string $extraMeta, string $expected): void
{
Injector::inst()->registerService(HTML4Value::create(), HTMLValue::class);
$siteTree = new SiteTree();
$siteTree->ExtraMeta = $extraMeta;
$siteTree->write();
$this->assertSame(
$expected,
$siteTree->ExtraMeta,
'Invalid HTML is converted to valid HTML and parsed'
);
}
/**
* @dataProvider provideSanatiseInvalidExtraMeta
*/
public function testSanatiseInvalidExtraMetaHTML5Value(string $extraMeta): void
{
// HTML5Value comes from the module silverstripe/html5
if (!class_exists(HTML5Value::class)) {
$this->markTestSkipped('HTML5Value class does not exist');
}
Injector::inst()->registerService(HTML5Value::create(), HTMLValue::class);
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('Custom Meta Tags does not contain valid HTML');
$siteTree = new SiteTree();
$siteTree->ExtraMeta = $extraMeta;
$siteTree->write();
}
public function provideSanatiseInvalidExtraMeta(): array
{
return [
[
'<link rel="canonical" href="valid" ;;// somethingdodgy <onmouseover=alert(1)>',
'<link rel="canonical" href="valid" somethingdodgy=""><onmouseover></onmouseover>',
'Invalid HTML is converted to valid HTML and parsed'
],
'<link rel="canonical" href="valid" ;;// somethingdodgy < onmouseover=alert(1)',
'<link rel="canonical" href="valid" somethingdodgy="">'
]
];
}

26
tests/php/Model/SiteTreeTest.yml Normal file → Executable file
View File

@ -44,7 +44,7 @@ SilverStripe\Security\Member:
securityadmin:
Groups: =>SilverStripe\Security\Group.securityadmins
SilverStripe\CMS\Model\SiteTree:
Page:
home:
Title: Home
CanEditType: OnlyTheseUsers
@ -56,14 +56,14 @@ SilverStripe\CMS\Model\SiteTree:
staff:
Title: Staff
URLSegment: my-staff
Parent: =>SilverStripe\CMS\Model\SiteTree.about
Parent: =>Page.about
ceo:
Title: CEO
Parent: =>SilverStripe\CMS\Model\SiteTree.staff
Parent: =>Page.staff
staffduplicate:
Title: Staff
URLSegment: my-staff
Parent: =>SilverStripe\CMS\Model\SiteTree.about
Parent: =>Page.about
ShowInMenus: 0
products:
Title: Products
@ -71,19 +71,19 @@ SilverStripe\CMS\Model\SiteTree:
EditorGroups: =>SilverStripe\Security\Group.editors
product1:
Title: 1.1 Test Product
Parent: =>SilverStripe\CMS\Model\SiteTree.products
Parent: =>Page.products
CanEditType: Inherit
product2:
Title: Another Product
Parent: =>SilverStripe\CMS\Model\SiteTree.products
Parent: =>Page.products
CanEditType: Inherit
product3:
Title: Another Product
Parent: =>SilverStripe\CMS\Model\SiteTree.products
Parent: =>Page.products
CanEditType: Inherit
product4:
Title: Another Product
Parent: =>SilverStripe\CMS\Model\SiteTree.products
Parent: =>Page.products
CanEditType: OnlyTheseUsers
EditorGroups: =>SilverStripe\Security\Group.admins
contact:
@ -102,16 +102,16 @@ SilverStripe\CMS\Model\SiteTree:
Title: 'Breadcrumbs'
breadcrumbs2:
Title: 'Breadcrumbs 2'
Parent: =>SilverStripe\CMS\Model\SiteTree.breadcrumbs
Parent: =>Page.breadcrumbs
breadcrumbs3:
Title: 'Breadcrumbs 3'
Parent: =>SilverStripe\CMS\Model\SiteTree.breadcrumbs2
Parent: =>Page.breadcrumbs2
breadcrumbs4:
Title: 'Breadcrumbs 4'
Parent: =>SilverStripe\CMS\Model\SiteTree.breadcrumbs3
Parent: =>Page.breadcrumbs3
breadcrumbs5:
Title: 'Breadcrumbs 5'
Parent: =>SilverStripe\CMS\Model\SiteTree.breadcrumbs4
Parent: =>Page.breadcrumbs4
numeric0:
Title: 'urlsegment is 0'
URLSegment: '0'
@ -130,7 +130,7 @@ SilverStripe\CMS\Model\RedirectorPage:
SilverStripe\CMS\Tests\Model\SiteTreeTest_DataObject:
relations:
Title: 'Linked DataObject'
Pages: =>SilverStripe\CMS\Model\SiteTree.home,=>SilverStripe\CMS\Model\SiteTree.about,=>SilverStripe\CMS\Model\SiteTree.staff
Pages: =>Page.home,=>Page.about,=>Page.staff
SilverStripe\CMS\Tests\Model\SiteTreeBrokenLinksTest\NotPageObject:
object1:

View File

@ -3,9 +3,9 @@
namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\Dev\TestOnly;
use SilverStripe\CMS\Model\SiteTree;
use Page;
class SiteTreeTest_AdminDenied extends SiteTree implements TestOnly
class SiteTreeTest_AdminDenied extends Page implements TestOnly
{
private static $table_name = 'SiteTreeTest_AdminDenied';

View File

@ -3,18 +3,18 @@
namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\Dev\TestOnly;
use SilverStripe\CMS\Model\SiteTree;
use Page;
class SiteTreeTest_ClassA extends SiteTree implements TestOnly
class SiteTreeTest_ClassA extends Page implements TestOnly
{
private static $table_name = 'SiteTreeTest_ClassA';
private static $need_permission = [
'ADMIN',
'CMS_ACCESS_CMSMain',
'CMS_ACCESS_CMSMain'
];
private static $allowed_children = [
SiteTreeTest_ClassB::class,
SiteTreeTest_ClassB::class
];
}

View File

@ -3,14 +3,14 @@
namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\Dev\TestOnly;
use SilverStripe\CMS\Model\SiteTree;
use Page;
class SiteTreeTest_ClassB extends SiteTree implements TestOnly
class SiteTreeTest_ClassB extends Page implements TestOnly
{
private static $table_name = 'SiteTreeTest_ClassB';
// Also allowed subclasses
private static $allowed_children = [
SiteTreeTest_ClassC::class,
SiteTreeTest_ClassC::class
];
}

View File

@ -3,9 +3,9 @@
namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\Dev\TestOnly;
use SilverStripe\CMS\Model\SiteTree;
use Page;
class SiteTreeTest_ClassC extends SiteTree implements TestOnly
class SiteTreeTest_ClassC extends Page implements TestOnly
{
private static $table_name = 'SiteTreeTest_ClassC';

View File

@ -10,6 +10,6 @@ class SiteTreeTest_ClassCext extends SiteTreeTest_ClassC implements TestOnly
// Override SiteTreeTest_ClassC definitions
private static $allowed_children = [
SiteTreeTest_ClassB::class,
SiteTreeTest_ClassB::class
];
}

View File

@ -3,9 +3,9 @@
namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\Dev\TestOnly;
use SilverStripe\CMS\Model\SiteTree;
use Page;
class SiteTreeTest_ClassD extends SiteTree implements TestOnly
class SiteTreeTest_ClassD extends Page implements TestOnly
{
private static $table_name = 'SiteTreeTest_ClassD';

View File

@ -4,9 +4,9 @@ namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\HiddenClass;
use SilverStripe\CMS\Model\SiteTree;
use Page;
class SiteTreeTest_ClassE extends SiteTree implements TestOnly, HiddenClass
class SiteTreeTest_ClassE extends Page implements TestOnly, HiddenClass
{
private static $table_name = 'SiteTreeTest_ClassE';
}

View File

@ -3,9 +3,9 @@
namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\Dev\TestOnly;
use SilverStripe\CMS\Model\SiteTree;
use Page;
class SiteTreeTest_Conflicted extends SiteTree implements TestOnly
class SiteTreeTest_Conflicted extends Page implements TestOnly
{
private static $table_name = 'SiteTreeTest_Conflicted';
}

View File

@ -3,12 +3,12 @@
namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\Dev\TestOnly;
use SilverStripe\CMS\Controllers\ContentController;
use PageController;
class SiteTreeTest_ConflictedController extends ContentController implements TestOnly
class SiteTreeTest_ConflictedController extends PageController implements TestOnly
{
private static $allowed_actions = [
'conflicted-action',
'conflicted-action'
];
public function hasActionTemplate($template)

View File

@ -3,12 +3,12 @@
namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\Dev\TestOnly;
use SilverStripe\CMS\Model\SiteTree;
use Page;
/**
* An empty SiteTree instance with a controller to test that legacy controller names can still be loaded
*/
class SiteTreeTest_LegacyControllerName extends SiteTree implements TestOnly
class SiteTreeTest_LegacyControllerName extends Page implements TestOnly
{
private static $table_name = 'SiteTreeTest_LegacyControllerName';
}

View File

@ -3,8 +3,9 @@
namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\Dev\TestOnly;
use SilverStripe\CMS\Controllers\ContentController;
use PageController;
class SiteTreeTest_LegacyControllerName_Controller extends ContentController implements TestOnly
class SiteTreeTest_LegacyControllerName_Controller extends PageController implements TestOnly
{
}

View File

@ -3,9 +3,9 @@
namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\Dev\TestOnly;
use SilverStripe\CMS\Model\SiteTree;
use Page;
class SiteTreeTest_NotRoot extends SiteTree implements TestOnly
class SiteTreeTest_NotRoot extends Page implements TestOnly
{
private static $table_name = 'SiteTreeTest_NotRoot';

View File

@ -3,9 +3,9 @@
namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\Dev\TestOnly;
use SilverStripe\CMS\Model\SiteTree;
use Page;
class SiteTreeTest_PageNode extends SiteTree implements TestOnly
class SiteTreeTest_PageNode extends Page implements TestOnly
{
private static $table_name = 'SiteTreeTest_PageNode';
}

View File

@ -3,8 +3,8 @@
namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\Dev\TestOnly;
use SilverStripe\CMS\Controllers\ContentController;
use PageController;
class SiteTreeTest_PageNodeController extends ContentController implements TestOnly
class SiteTreeTest_PageNodeController extends PageController implements TestOnly
{
}

View File

@ -2,6 +2,7 @@
namespace SilverStripe\CMS\Tests\Model;
use Page;
use SilverStripe\CMS\Controllers\ModelAsController;
use SilverStripe\CMS\Model\RedirectorPage;
use SilverStripe\CMS\Model\SiteTree;
@ -42,8 +43,8 @@ class VirtualPageTest extends FunctionalTest
protected static $required_extensions = [
SiteTree::class => [
VirtualPageTest_PageExtension::class,
],
VirtualPageTest_PageExtension::class
]
];
protected function setUp(): void
@ -62,8 +63,8 @@ class VirtualPageTest extends FunctionalTest
);
// Ensure all pages are published
/** @var SiteTree $page */
foreach (SiteTree::get() as $page) {
/** @var Page $page */
foreach (Page::get() as $page) {
$page->publishSingle();
}
}
@ -74,8 +75,8 @@ class VirtualPageTest extends FunctionalTest
*/
public function testEditingSourcePageUpdatesVirtualPages()
{
/** @var SiteTree $master */
$master = $this->objFromFixture(SiteTree::class, 'master');
/** @var Page $master */
$master = $this->objFromFixture('Page', 'master');
$master->Title = "New title";
$master->MenuTitle = "New menutitle";
$master->Content = "<p>New content</p>";
@ -96,7 +97,7 @@ class VirtualPageTest extends FunctionalTest
public function testMetaTags()
{
$this->logInWithPermission('ADMIN');
$master = $this->objFromFixture(SiteTree::class, 'master');
$master = $this->objFromFixture('Page', 'master');
$vp1 = $this->objFromFixture(VirtualPage::class, 'vp1');
// Test with title
@ -117,8 +118,8 @@ class VirtualPageTest extends FunctionalTest
{
$this->logInWithPermission('ADMIN');
/** @var SiteTree $master */
$master = $this->objFromFixture(SiteTree::class, 'master');
/** @var Page $master */
$master = $this->objFromFixture('Page', 'master');
$master->publishRecursive();
$master->Title = "New title";
@ -159,13 +160,13 @@ class VirtualPageTest extends FunctionalTest
$vp = new VirtualPage();
$vp->write();
$vp->CopyContentFromID = $this->idFromFixture(SiteTree::class, 'master');
$vp->CopyContentFromID = $this->idFromFixture('Page', 'master');
$vp->write();
$this->assertEquals("My Page", $vp->Title);
$this->assertEquals("My Page Nav", $vp->MenuTitle);
$vp->CopyContentFromID = $this->idFromFixture(SiteTree::class, 'master2');
$vp->CopyContentFromID = $this->idFromFixture('Page', 'master2');
$vp->write();
$this->assertEquals("My Other Page", $vp->Title);
@ -179,7 +180,7 @@ class VirtualPageTest extends FunctionalTest
*/
public function testPublishingAVirtualPageCopiedPublishedContentNotDraftContent()
{
$p = new SiteTree();
$p = new Page();
$p->Content = "published content";
$p->write();
$p->publishRecursive();
@ -219,7 +220,7 @@ class VirtualPageTest extends FunctionalTest
public function testCantPublishVirtualPagesBeforeTheirSource()
{
// An unpublished source page
$p = new SiteTree();
$p = new Page();
$p->Content = "test content";
$p->write();
@ -240,7 +241,7 @@ class VirtualPageTest extends FunctionalTest
public function testCanEdit()
{
$parentPage = $this->objFromFixture(SiteTree::class, 'master3');
$parentPage = $this->objFromFixture('Page', 'master3');
$virtualPage = $this->objFromFixture(VirtualPage::class, 'vp3');
$bob = $this->objFromFixture(Member::class, 'bob');
$andrew = $this->objFromFixture(Member::class, 'andrew');
@ -258,8 +259,8 @@ class VirtualPageTest extends FunctionalTest
public function testCanView()
{
/** @var SiteTree $parentPage */
$parentPage = $this->objFromFixture(SiteTree::class, 'master3');
/** @var Page $parentPage */
$parentPage = $this->objFromFixture('Page', 'master3');
$parentPage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
/** @var VirtualPage $virtualPage */
@ -282,7 +283,7 @@ class VirtualPageTest extends FunctionalTest
public function testVirtualPagesArentInappropriatelyPublished()
{
// Fixture
$p = new SiteTree();
$p = new Page();
$p->Content = "test content";
$p->write();
$vp = new VirtualPage();
@ -331,7 +332,7 @@ class VirtualPageTest extends FunctionalTest
public function testUnpublishingSourcePageOfAVirtualPageAlsoUnpublishesVirtualPage()
{
// Create page and virutal page
$p = new SiteTree();
$p = new Page();
$p->Title = "source";
$p->write();
$this->assertTrue($p->publishRecursive());
@ -363,7 +364,7 @@ class VirtualPageTest extends FunctionalTest
public function testDeletingFromLiveSourcePageOfAVirtualPageAlsoUnpublishesVirtualPage()
{
// Create page and virutal page
$p = new SiteTree();
$p = new Page();
$p->Title = "source";
$p->write();
$this->assertTrue($p->publishRecursive());
@ -383,7 +384,7 @@ class VirtualPageTest extends FunctionalTest
->byID($pID);
$this->assertNull($vpDraft);
// Delete the source page form live, confirm that the virtual page has also been unpublished
/** @var SiteTree $pLive */
/** @var Page $pLive */
$pLive = Versioned::get_by_stage(SiteTree::class, Versioned::LIVE)
->byID($pID);
$this->assertTrue($pLive->doUnpublish());

View File

@ -31,7 +31,7 @@ SilverStripe\Security\Member:
alice:
Email: alice@alice.com
Groups: =>SilverStripe\Security\Group.alicegroup
SilverStripe\CMS\Model\SiteTree:
Page:
master:
Title: My Page
MenuTitle: My Page Nav
@ -52,15 +52,15 @@ SilverStripe\CMS\Tests\Model\VirtualPageTest_ClassA:
SilverStripe\CMS\Model\VirtualPage:
vp1:
Title: vp1
CopyContentFrom: =>SilverStripe\CMS\Model\SiteTree.master
Parent: =>SilverStripe\CMS\Model\SiteTree.holder
CopyContentFrom: =>Page.master
Parent: =>Page.holder
vp2:
Title: vp2
CopyContentFrom: =>SilverStripe\CMS\Model\SiteTree.master
Parent: =>SilverStripe\CMS\Model\SiteTree.holder
CopyContentFrom: =>Page.master
Parent: =>Page.holder
vp3:
CopyContentFrom: =>SilverStripe\CMS\Model\SiteTree.master3
Parent: =>SilverStripe\CMS\Model\SiteTree.holder
CopyContentFrom: =>Page.master3
Parent: =>Page.holder
CanEditType: OnlyTheseUsers
CanViewType: OnlyTheseUsers
EditorGroups: =>SilverStripe\Security\Group.andrewgroup

View File

@ -3,9 +3,9 @@
namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\Dev\TestOnly;
use SilverStripe\CMS\Model\SiteTree;
use Page;
class VirtualPageTest_ClassA extends SiteTree implements TestOnly
class VirtualPageTest_ClassA extends Page implements TestOnly
{
private static $table_name = 'VirtualPageTest_ClassA';

View File

@ -3,12 +3,12 @@
namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\Dev\TestOnly;
use SilverStripe\CMS\Controllers\ContentController;
use PageController;
class VirtualPageTest_ClassAController extends ContentController implements TestOnly
class VirtualPageTest_ClassAController extends PageController implements TestOnly
{
private static $allowed_actions = [
'testaction',
'testaction'
];
public function testMethod()

View File

@ -3,9 +3,9 @@
namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\Dev\TestOnly;
use SilverStripe\CMS\Model\SiteTree;
use Page;
class VirtualPageTest_ClassB extends SiteTree implements TestOnly
class VirtualPageTest_ClassB extends Page implements TestOnly
{
private static $table_name = 'VirtualPageTest_ClassB';

View File

@ -3,9 +3,9 @@
namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\Dev\TestOnly;
use SilverStripe\CMS\Model\SiteTree;
use Page;
class VirtualPageTest_ClassC extends SiteTree implements TestOnly
class VirtualPageTest_ClassC extends Page implements TestOnly
{
private static $table_name = 'VirtualPageTest_ClassC';

View File

@ -3,9 +3,9 @@
namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\Dev\TestOnly;
use SilverStripe\CMS\Model\SiteTree;
use Page;
class VirtualPageTest_NotRoot extends SiteTree implements TestOnly
class VirtualPageTest_NotRoot extends Page implements TestOnly
{
private static $table_name = 'VirtualPageTest_NotRoot';

View File

@ -9,8 +9,7 @@ class VirtualPageTest_PageExtension extends DataExtension implements TestOnly
{
private static $db = [
// These fields are just on an extension to simulate shared properties between Page and VirtualPage.
// Not possible through direct $db definitions due to VirtualPage inheriting from Page,
// and Page being defined elsewhere.
// Not possible through direct $db definitions due to VirtualPage inheriting from Page, and Page being defined elsewhere.
'MySharedVirtualField' => 'Text',
'MySharedNonVirtualField' => 'Text',
];

View File

@ -4,9 +4,9 @@ namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\CMS\Model\VirtualPage;
use SilverStripe\Dev\TestOnly;
use SilverStripe\CMS\Model\SiteTree;
use Page;
class VirtualPageTest_PageWithAllowedChildren extends SiteTree implements TestOnly
class VirtualPageTest_PageWithAllowedChildren extends Page implements TestOnly
{
private static $table_name = 'VirtualPageTest_PageWithAllowedChildren';

View File

@ -2,22 +2,24 @@
namespace SilverStripe\CMS\Tests\Reports;
use SilverStripe\Assets\File;
use SilverStripe\CMS\Model\RedirectorPage;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\CMS\Model\VirtualPage;
use SilverStripe\CMS\Reports\BrokenFilesReport;
use SilverStripe\CMS\Reports\BrokenLinksReport;
use SilverStripe\CMS\Reports\BrokenRedirectorPagesReport;
use SilverStripe\CMS\Reports\BrokenVirtualPagesReport;
use SilverStripe\CMS\Reports\RecentlyEditedReport;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\CMS\Reports\RecentlyEditedReport;
use SilverStripe\CMS\Reports\BrokenLinksReport;
use SilverStripe\CMS\Reports\BrokenFilesReport;
use SilverStripe\CMS\Model\VirtualPage;
use SilverStripe\CMS\Reports\BrokenVirtualPagesReport;
use SilverStripe\CMS\Model\RedirectorPage;
use SilverStripe\CMS\Reports\BrokenRedirectorPagesReport;
use SilverStripe\Reports\Report;
use SilverStripe\Assets\File;
use SilverStripe\Dev\SapphireTest;
use Page;
class CmsReportsTest extends SapphireTest
{
protected static $fixture_file = 'CmsReportsTest.yml';
private static $daysAgo = 14;
@ -27,19 +29,14 @@ class CmsReportsTest extends SapphireTest
parent::setUp();
// set the dates by hand: impossible to set via yml
$afterThreshold = strtotime('-' . (self::$daysAgo - 1) . ' days', strtotime('31-06-2009 00:00:00'));
$beforeThreshold = strtotime('-' . (self::$daysAgo + 1) . ' days', strtotime('31-06-2009 00:00:00'));
$afterThreshold = strtotime('-'.(self::$daysAgo-1).' days', strtotime('31-06-2009 00:00:00'));
$beforeThreshold = strtotime('-'.(self::$daysAgo+1).' days', strtotime('31-06-2009 00:00:00'));
$after = $this->objFromFixture(SiteTree::class, 'after');
$before = $this->objFromFixture(SiteTree::class, 'before');
DB::query(
"UPDATE \"SiteTree\" SET \"Created\"='2009-01-01 00:00:00', \"LastEdited\"='" .
date('Y-m-d H:i:s', $afterThreshold) . "' WHERE \"ID\"='" . $after->ID . "'"
);
DB::query(
"UPDATE \"SiteTree\" SET \"Created\"='2009-01-01 00:00:00', \"LastEdited\"='" .
date('Y-m-d H:i:s', $beforeThreshold) . "' WHERE \"ID\"='" . $before->ID . "'"
);
DB::query("UPDATE \"SiteTree\" SET \"Created\"='2009-01-01 00:00:00', \"LastEdited\"='".date('Y-m-d H:i:s', $afterThreshold)."' WHERE \"ID\"='".$after->ID."'");
DB::query("UPDATE \"SiteTree\" SET \"Created\"='2009-01-01 00:00:00', \"LastEdited\"='".date('Y-m-d H:i:s', $beforeThreshold)."' WHERE \"ID\"='".$before->ID."'");
}
/**
@ -55,27 +52,13 @@ class CmsReportsTest extends SapphireTest
// ASSERT that the "draft" report is returning the correct results.
$parameters = ['CheckSite' => 'Draft'];
$results = count($report->sourceRecords($parameters, null, null)) > 0;
$isDraftBroken
? $this->assertTrue(
$results,
"{$class} has NOT returned the correct DRAFT results, as NO pages were found."
) : $this->assertFalse(
$results,
"{$class} has NOT returned the correct DRAFT results, as pages were found."
);
$results = count($report->sourceRecords($parameters, null, null) ?? []) > 0;
$isDraftBroken ? $this->assertTrue($results, "{$class} has NOT returned the correct DRAFT results, as NO pages were found.") : $this->assertFalse($results, "{$class} has NOT returned the correct DRAFT results, as pages were found.");
// ASSERT that the "published" report is returning the correct results.
$parameters = ['CheckSite' => 'Published', 'OnLive' => 1];
$results = count($report->sourceRecords($parameters, null, null) ?? []) > 0;
$isPublishedBroken
? $this->assertTrue(
$results,
"{$class} has NOT returned the correct PUBLISHED results, as NO pages were found."
) : $this->assertFalse(
$results,
"{$class} has NOT returned the correct PUBLISHED results, as pages were found."
);
$isPublishedBroken ? $this->assertTrue($results, "{$class} has NOT returned the correct PUBLISHED results, as NO pages were found.") : $this->assertFalse($results, "{$class} has NOT returned the correct PUBLISHED results, as pages were found.");
}
public function testRecentlyEdited()
@ -98,14 +81,18 @@ class CmsReportsTest extends SapphireTest
/**
* Test the broken links side report.
*/
public function testBrokenLinks()
{
// Create a "draft" page with a broken link.
$page = new SiteTree();
$page = Page::create();
$page->Content = "<a href='[sitetree_link,id=987654321]'>This</a> is a broken link.";
$page->writeToStage('Stage');
// Retrieve the broken links side report.
$reports = Report::get_reports();
$brokenLinksReport = null;
foreach ($reports as $report) {
@ -122,11 +109,12 @@ class CmsReportsTest extends SapphireTest
}
// ASSERT that the "draft" report has detected the page having a broken link.
// ASSERT that the "published" report has NOT detected the page having a broken link,
// as the page has not been "published" yet.
// ASSERT that the "published" report has NOT detected the page having a broken link, as the page has not been "published" yet.
$this->isReportBroken($brokenLinksReport, true, false);
// Make sure the page is now "published".
$page->writeToStage('Live');
// ASSERT that the "draft" report has detected the page having a broken link.
@ -135,15 +123,17 @@ class CmsReportsTest extends SapphireTest
$this->isReportBroken($brokenLinksReport, true, true);
// Correct the "draft" broken link.
$page->Content = str_replace('987654321', $page->ID ?? '', $page->Content ?? '');
$page->writeToStage('Stage');
// ASSERT that the "draft" report has NOT detected the page having a broken link.
// ASSERT that the "published" report has detected the page having a broken link,
// as the previous content remains "published".
// ASSERT that the "published" report has detected the page having a broken link, as the previous content remains "published".
$this->isReportBroken($brokenLinksReport, false, true);
// Make sure the change has now been "published".
$page->writeToStage('Live');
// ASSERT that the "draft" report has NOT detected the page having a broken link.
@ -155,14 +145,18 @@ class CmsReportsTest extends SapphireTest
/**
* Test the broken files side report.
*/
public function testBrokenFiles()
{
// Create a "draft" page with a broken file.
$page = new SiteTree();
$page = Page::create();
$page->Content = "<a href='[file_link,id=987654321]'>This</a> is a broken file.";
$page->writeToStage('Stage');
// Retrieve the broken files side report.
$reports = Report::get_reports();
$brokenFilesReport = null;
foreach ($reports as $report) {
@ -179,18 +173,21 @@ class CmsReportsTest extends SapphireTest
}
// ASSERT that the "draft" report has detected the page having a broken file.
// ASSERT that the "published" report has NOT detected the page having a broken file,
// as the page has not been "published" yet.
// ASSERT that the "published" report has NOT detected the page having a broken file, as the page has not been "published" yet.
$this->isReportBroken($brokenFilesReport, true, false);
// Make sure the page is now "published".
$page->writeToStage('Live');
// ASSERT that the "draft" report has detected the page having a broken file.
// ASSERT that the "published" report has detected the page having a broken file.
$this->isReportBroken($brokenFilesReport, true, true);
// Correct the "draft" broken file.
$file = File::create();
$file->Filename = 'name.pdf';
$file->write();
@ -198,15 +195,17 @@ class CmsReportsTest extends SapphireTest
$page->writeToStage('Stage');
// ASSERT that the "draft" report has NOT detected the page having a broken file.
// ASSERT that the "published" report has detected the page having a broken file,
// as the previous content remains "published".
// ASSERT that the "published" report has detected the page having a broken file, as the previous content remains "published".
$this->isReportBroken($brokenFilesReport, false, true);
// Make sure the change has now been "published".
$page->writeToStage('Live');
// ASSERT that the "draft" report has NOT detected the page having a broken file.
// ASSERT that the "published" report has NOT detected the page having a broken file.
$this->isReportBroken($brokenFilesReport, false, false);
}
@ -216,12 +215,15 @@ class CmsReportsTest extends SapphireTest
public function testBrokenVirtualPages()
{
// Create a "draft" virtual page with a broken link.
$page = VirtualPage::create();
$page->CopyContentFromID = 987654321;
$page->writeToStage('Stage');
// Retrieve the broken virtual pages side report.
$reports = Report::get_reports();
$brokenVirtualPagesReport = null;
foreach ($reports as $report) {
@ -238,19 +240,22 @@ class CmsReportsTest extends SapphireTest
}
// ASSERT that the "draft" report has detected the page having a broken link.
// ASSERT that the "published" report has NOT detected the page having a broken link,
// as the page has not been "published" yet.
// ASSERT that the "published" report has NOT detected the page having a broken link, as the page has not been "published" yet.
$this->isReportBroken($brokenVirtualPagesReport, true, false);
// Make sure the page is now "published".
$page->writeToStage('Live');
// ASSERT that the "draft" report has detected the page having a broken link.
// ASSERT that the "published" report has detected the page having a broken link.
$this->isReportBroken($brokenVirtualPagesReport, true, true);
// Correct the "draft" broken link.
$contentPage = new SiteTree();
$contentPage = Page::create();
$contentPage->Content = 'This is some content.';
$contentPage->writeToStage('Stage');
$contentPage->writeToStage('Live');
@ -258,30 +263,36 @@ class CmsReportsTest extends SapphireTest
$page->writeToStage('Stage');
// ASSERT that the "draft" report has NOT detected the page having a broken link.
// ASSERT that the "published" report has detected the page having a broken link,
// as the previous content remains "published".
// ASSERT that the "published" report has detected the page having a broken link, as the previous content remains "published".
$this->isReportBroken($brokenVirtualPagesReport, false, true);
// Make sure the change has now been "published".
$page->writeToStage('Live');
// ASSERT that the "draft" report has NOT detected the page having a broken link.
// ASSERT that the "published" report has NOT detected the page having a broken link.
$this->isReportBroken($brokenVirtualPagesReport, false, false);
}
/**
* Test the broken redirector pages side report.
*/
public function testBrokenRedirectorPages()
{
// Create a "draft" redirector page with a broken link.
$page = RedirectorPage::create();
$page->RedirectionType = 'Internal';
$page->LinkToID = 987654321;
$page->writeToStage('Stage');
// Retrieve the broken redirector pages side report.
$reports = Report::get_reports();
$brokenRedirectorPagesReport = null;
foreach ($reports as $report) {
@ -298,19 +309,22 @@ class CmsReportsTest extends SapphireTest
}
// ASSERT that the "draft" report has detected the page having a broken link.
// ASSERT that the "published" report has NOT detected the page having a broken link,
// as the page has not been "published" yet.
// ASSERT that the "published" report has NOT detected the page having a broken link, as the page has not been "published" yet.
$this->isReportBroken($brokenRedirectorPagesReport, true, false);
// Make sure the page is now "published".
$page->writeToStage('Live');
// ASSERT that the "draft" report has detected the page having a broken link.
// ASSERT that the "published" report has detected the page having a broken link.
$this->isReportBroken($brokenRedirectorPagesReport, true, true);
// Correct the "draft" broken link.
$contentPage = new SiteTree();
$contentPage = Page::create();
$contentPage->Content = 'This is some content.';
$contentPage->writeToStage('Stage');
$contentPage->writeToStage('Live');
@ -318,15 +332,17 @@ class CmsReportsTest extends SapphireTest
$page->writeToStage('Stage');
// ASSERT that the "draft" report has NOT detected the page having a broken link.
// ASSERT that the "published" report has detected the page having a broken link,
// as the previous content remains "published".
// ASSERT that the "published" report has detected the page having a broken link, as the previous content remains "published".
$this->isReportBroken($brokenRedirectorPagesReport, false, true);
// Make sure the change has now been "published".
$page->writeToStage('Live');
// ASSERT that the "draft" report has NOT detected the page having a broken link.
// ASSERT that the "published" report has NOT detected the page having a broken link.
$this->isReportBroken($brokenRedirectorPagesReport, false, false);
}
}

View File

@ -20,7 +20,7 @@ class CMSMainSearchFormTest extends FunctionalTest
'q' => [
'Term' => 'Page 10',
'FilterClass' => CMSSiteTreeFilter_Search::class,
],
]
])
);

View File

@ -2,6 +2,7 @@
namespace SilverStripe\CMS\Tests\Search;
use Page;
use SilverStripe\Assets\File;
use SilverStripe\CMS\Controllers\ContentController;
use SilverStripe\CMS\Controllers\ModelAsController;
@ -21,10 +22,14 @@ use SilverStripe\Versioned\Versioned;
use TractorCow\Fluent\Extension\FluentSiteTreeExtension;
/**
* @todo Fix unpublished pages check in testPublishedPagesMatchedByTitle()
* @todo All tests run on unpublished pages at the moment, due to the searchform not distinguishing between them
*
* Because this manipulates the test database in severe ways, I've renamed the test to force it to run last...
*/
class ZZZSearchFormTest extends FunctionalTest
{
protected static $fixture_file = 'ZZZSearchFormTest.yml';
protected static $illegal_extensions = [
@ -92,6 +97,9 @@ class ZZZSearchFormTest extends FunctionalTest
return $supports;
}
/**
* @skipUpgrade
*/
public function testSearchFormTemplateCanBeChanged()
{
if (!$this->checkFulltextSupport()) {
@ -108,6 +116,9 @@ class ZZZSearchFormTest extends FunctionalTest
);
}
/**
* @skipUpgrade
*/
public function testPublishedPagesMatchedByTitle()
{
if (!$this->checkFulltextSupport()) {
@ -138,6 +149,9 @@ class ZZZSearchFormTest extends FunctionalTest
);
}
/**
* @skipUpgrade
*/
public function testDoubleQuotesPublishedPagesMatchedByTitle()
{
if (!$this->checkFulltextSupport()) {
@ -170,6 +184,9 @@ class ZZZSearchFormTest extends FunctionalTest
);
}
/**
* @skipUpgrade
*/
public function testUnpublishedPagesNotIncluded()
{
if (!$this->checkFulltextSupport()) {

View File

@ -0,0 +1,91 @@
<?php
namespace SilverStripe\CMS\Tests\Tasks;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\CMS\Tasks\MigrateSiteTreeLinkingTask;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\Dev\Deprecation;
class MigrateSiteTreeLinkingTaskTest extends SapphireTest
{
protected static $fixture_file = 'MigrateSiteTreeLinkingTaskTest.yml';
public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();
// Cover db reset in case parent did not start
if (!static::getExtraDataObjects()) {
DataObject::reset();
static::resetDBSchema(true, true);
}
// Ensure legacy SiteTree_LinkTracking table exists
DB::get_schema()->schemaUpdate(function () {
DB::require_table('SiteTree_LinkTracking', [
'SiteTreeID' => 'Int',
'ChildID' => 'Int',
'FieldName' => 'Varchar',
]);
});
}
protected function setUp(): void
{
if (Deprecation::isEnabled()) {
$this->markTestSkipped('Test calls deprecated code');
}
parent::setUp();
// Manually bootstrap all Content blocks with soft coded IDs (raw sql to avoid save hooks)
$replacements = [
'$$ABOUTID$$' => $this->idFromFixture(SiteTree::class, 'about'),
'$$HOMEID$$' => $this->idFromFixture(SiteTree::class, 'home'),
'$$STAFFID$$' => $this->idFromFixture(SiteTree::class, 'staff'),
];
foreach (DB::query('SELECT "ID", "Content" FROM "SiteTree"') as $row) {
$id = (int)$row['ID'];
$content = str_replace(array_keys($replacements ?? []), array_values($replacements ?? []), $row['Content'] ?? '');
DB::prepared_query('UPDATE "SiteTree" SET "Content" = ? WHERE "ID" = ?', [$content, $id]);
}
DataObject::reset();
}
public function testLinkingMigration()
{
ob_start();
DB::quiet(false);
$task = new MigrateSiteTreeLinkingTask();
$task->run(null);
$this->assertStringContainsString(
"Migrated page links on 5 Pages",
ob_get_contents(),
'Rewritten links are correctly reported'
);
DB::quiet(true);
ob_end_clean();
// Query links for pages
/** @var SiteTree $home */
$home = $this->objFromFixture(SiteTree::class, 'home');
/** @var SiteTree $about */
$about = $this->objFromFixture(SiteTree::class, 'about');
/** @var SiteTree $staff */
$staff = $this->objFromFixture(SiteTree::class, 'staff');
/** @var SiteTree $action */
$action = $this->objFromFixture(SiteTree::class, 'action');
/** @var SiteTree $hash */
$hash = $this->objFromFixture(SiteTree::class, 'hash_link');
// Ensure all links are created
$this->assertListEquals([['ID' => $about->ID], ['ID' => $staff->ID]], $home->LinkTracking());
$this->assertListEquals([['ID' => $home->ID], ['ID' => $staff->ID]], $about->LinkTracking());
$this->assertListEquals([['ID' => $home->ID], ['ID' => $about->ID]], $staff->LinkTracking());
$this->assertListEquals([['ID' => $home->ID]], $action->LinkTracking());
$this->assertListEquals([['ID' => $home->ID], ['ID' => $about->ID]], $hash->LinkTracking());
}
}

View File

@ -0,0 +1,67 @@
SilverStripe\CMS\Model\SiteTree:
home:
Title: Home Page
URLSegment: home
Content: '<a href="[sitetree_link,id=$$ABOUTID$$]">About</a><a href="[sitetree_link,id=$$STAFFID$$]">Staff</a><a href="http://silverstripe.org/">External Link</a><a name="anchor"></a>'
about:
Title: About Us
URLSegment: about
Content: '<a href="[sitetree_link,id=$$HOMEID$$]">Home</a><a href="[sitetree_link,id=$$STAFFID$$]">Staff</a><a name="second-anchor"></a>'
staff:
Title: Staff
URLSegment: staff
Content: '<a href="[sitetree_link,id=$$HOMEID$$]">Home</a><a href="[sitetree_link,id=$$ABOUTID$$]">About</a>'
Parent: =>SilverStripe\CMS\Model\SiteTree.about
action:
Title: Action Link
URLSegment: action
Content: '<a href="[sitetree_link,id=$$HOMEID$$]SearchForm">Search Form</a>'
hash_link:
Title: Hash Link
URLSegment: hash-link
Content: '<a href="[sitetree_link,id=$$HOMEID$$]#anchor">Home</a><a href="[sitetree_link,id=$$ABOUTID$$]#second-anchor">About</a>'
admin_link:
Title: Admin Link
URLSegment: admin-link
Content: '<a href="admin">Admin</a>'
no_links:
Title: No Links
URLSegment: No Links
SiteTree_LinkTracking:
home_about:
SiteTreeID: =>SilverStripe\CMS\Model\SiteTree.home
ChildID: =>SilverStripe\CMS\Model\SiteTree.about
FieldName: Content
home_staff:
SiteTreeID: =>SilverStripe\CMS\Model\SiteTree.home
ChildID: =>SilverStripe\CMS\Model\SiteTree.staff
FieldName: Content
about_home:
SiteTreeID: =>SilverStripe\CMS\Model\SiteTree.about
ChildID: =>SilverStripe\CMS\Model\SiteTree.home
FieldName: Content
about_staff:
SiteTreeID: =>SilverStripe\CMS\Model\SiteTree.about
ChildID: =>SilverStripe\CMS\Model\SiteTree.staff
FieldName: Content
staff_home:
SiteTreeID: =>SilverStripe\CMS\Model\SiteTree.staff
ChildID: =>SilverStripe\CMS\Model\SiteTree.home
FieldName: Content
staff_about:
SiteTreeID: =>SilverStripe\CMS\Model\SiteTree.staff
ChildID: =>SilverStripe\CMS\Model\SiteTree.about
FieldName: Content
action_home:
SiteTreeID: =>SilverStripe\CMS\Model\SiteTree.action
ChildID: =>SilverStripe\CMS\Model\SiteTree.home
FieldName: Content
hash_link_home:
SiteTreeID: =>SilverStripe\CMS\Model\SiteTree.hash_link
ChildID: =>SilverStripe\CMS\Model\SiteTree.home
FieldName: Content
hash_link_about:
SiteTreeID: =>SilverStripe\CMS\Model\SiteTree.hash_link
ChildID: =>SilverStripe\CMS\Model\SiteTree.about
FieldName: Content

View File

@ -0,0 +1,113 @@
<?php
namespace SilverStripe\CMS\Tests\Tasks;
use SilverStripe\CMS\Tasks\RemoveOrphanedPagesTask;
use SilverStripe\Versioned\Versioned;
use SilverStripe\Dev\FunctionalTest;
use SilverStripe\Dev\Deprecation;
/**
* <h2>Fixture tree</h2>
* <code>
* parent1_published
* child1_1_published
* grandchild1_1_1
* grandchild1_1_2_published
* grandchild1_1_3_orphaned
* grandchild1_1_4_orphaned_published
* child1_2_published
* child1_3_orphaned
* child1_4_orphaned_published
* parent2
* child2_1_published_orphaned // is orphaned because parent is not published
* </code>
*
* <h2>Cleaned up tree</h2>
* <code>
* parent1_published
* child1_1_published
* grandchild1_1_1
* grandchild1_1_2_published
* child2_1_published_orphaned
* parent2
* </code>
*
* @author Ingo Schommer (<firstname>@silverstripe.com), SilverStripe Ltd.
*/
class RemoveOrphanedPagesTaskTest extends FunctionalTest
{
protected static $fixture_file = 'RemoveOrphanedPagesTaskTest.yml';
protected function setUp(): void
{
if (Deprecation::isEnabled()) {
$this->markTestSkipped('Test calls deprecated code');
}
parent::setUp();
$parent1_published = $this->objFromFixture('Page', 'parent1_published');
$parent1_published->publishSingle();
$child1_1_published = $this->objFromFixture('Page', 'child1_1_published');
$child1_1_published->publishSingle();
$child1_2_published = $this->objFromFixture('Page', 'child1_2_published');
$child1_2_published->publishSingle();
$child1_3_orphaned = $this->objFromFixture('Page', 'child1_3_orphaned');
$child1_3_orphaned->ParentID = 9999;
$child1_3_orphaned->write();
$child1_4_orphaned_published = $this->objFromFixture('Page', 'child1_4_orphaned_published');
$child1_4_orphaned_published->ParentID = 9999;
$child1_4_orphaned_published->write();
$child1_4_orphaned_published->publishSingle();
$grandchild1_1_2_published = $this->objFromFixture('Page', 'grandchild1_1_2_published');
$grandchild1_1_2_published->publishSingle();
$grandchild1_1_3_orphaned = $this->objFromFixture('Page', 'grandchild1_1_3_orphaned');
$grandchild1_1_3_orphaned->ParentID = 9999;
$grandchild1_1_3_orphaned->write();
$grandchild1_1_4_orphaned_published = $this->objFromFixture(
'Page',
'grandchild1_1_4_orphaned_published'
);
$grandchild1_1_4_orphaned_published->ParentID = 9999;
$grandchild1_1_4_orphaned_published->write();
$grandchild1_1_4_orphaned_published->publishSingle();
$child2_1_published_orphaned = $this->objFromFixture('Page', 'child2_1_published_orphaned');
$child2_1_published_orphaned->publishSingle();
}
public function testGetOrphansByStage()
{
// all orphans
$child1_3_orphaned = $this->objFromFixture('Page', 'child1_3_orphaned');
$child1_4_orphaned_published = $this->objFromFixture('Page', 'child1_4_orphaned_published');
$grandchild1_1_3_orphaned = $this->objFromFixture('Page', 'grandchild1_1_3_orphaned');
$grandchild1_1_4_orphaned_published = $this->objFromFixture(
'Page',
'grandchild1_1_4_orphaned_published'
);
$child2_1_published_orphaned = $this->objFromFixture('Page', 'child2_1_published_orphaned');
$task = singleton(RemoveOrphanedPagesTask::class);
$orphans = $task->getOrphanedPages();
$orphanIDs = $orphans->column('ID');
sort($orphanIDs);
$compareIDs = [
$child1_3_orphaned->ID,
$child1_4_orphaned_published->ID,
$grandchild1_1_3_orphaned->ID,
$grandchild1_1_4_orphaned_published->ID,
$child2_1_published_orphaned->ID
];
sort($compareIDs);
$this->assertEquals($orphanIDs, $compareIDs);
}
}

View File

@ -0,0 +1,32 @@
Page:
parent1_published:
Title: Parent1
child1_1_published:
Title: Child1.1
Parent: =>Page.parent1_published
child1_2_published:
Title: Child1.2
Parent: =>Page.parent1_published
child1_3_orphaned:
Title: Child1.3
Parent: =>Page.parent1_published
child1_4_orphaned_published:
Title: Child1.4
Parent: =>Page.parent1_published
grandchild1_1_1:
Title: Grandchild1.1.1
Parent: =>Page.child1_1_published
grandchild1_1_2_published:
Title: Grandchild1.1.2
Parent: =>Page.child1_1_published
grandchild1_1_3_orphaned:
Title: Grandchild1.1.3
Parent: =>Page.child1_1_published
grandchild1_1_4_orphaned_published:
Title: Grandchild1.1.4
Parent: =>Page.child1_1_published
parent2:
Title: Parent2
child2_1_published_orphaned:
Title: Child2.1
Parent: =>Page.parent2

View File

@ -1,8 +1,19 @@
const Path = require('path');
const { JavascriptWebpackConfig, CssWebpackConfig } = require('@silverstripe/webpack-config');
const webpackConfig = require('@silverstripe/webpack-config');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const {
resolveJS,
externalJS,
moduleJS,
pluginJS,
moduleCSS,
pluginCSS,
} = webpackConfig;
const ENV = process.env.NODE_ENV;
const PATHS = {
MODULES: 'node_modules',
FILES_PATH: '../',
ROOT: Path.resolve(),
SRC: Path.resolve('client/src'),
DIST: Path.resolve('client/dist'),
@ -10,37 +21,46 @@ const PATHS = {
};
const config = [
// Main JS bundles
new JavascriptWebpackConfig('js', PATHS, 'silverstripe/cms')
.setEntry({
{
name: 'js',
entry: {
bundle: `${PATHS.SRC}/bundles/bundle.js`,
// See https://github.com/webpack/webpack/issues/300#issuecomment-45313650
SilverStripeNavigator: `${PATHS.LEGACY_SRC}/SilverStripeNavigator.js`,
'TinyMCE_sslink-internal': `${PATHS.LEGACY_SRC}/TinyMCE_sslink-internal.js`,
'TinyMCE_sslink-anchor': `${PATHS.LEGACY_SRC}/TinyMCE_sslink-anchor.js`,
})
.mergeConfig({
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: `${PATHS.SRC}/images`,
to: `${PATHS.DIST}/images`
},
]
}),
],
})
.getConfig(),
// sass to css
new CssWebpackConfig('css', PATHS)
.setEntry({
},
output: {
path: PATHS.DIST,
filename: 'js/[name].js',
},
devtool: (ENV !== 'production') ? 'source-map' : '',
resolve: resolveJS(ENV, PATHS),
externals: externalJS(ENV, PATHS),
module: moduleJS(ENV, PATHS),
plugins: pluginJS(ENV, PATHS).concat([
new CopyWebpackPlugin([
{ from: 'client/src/images', to: 'images' },
])
])
},
{
name: 'css',
entry: {
bundle: `${PATHS.SRC}/styles/bundle.scss`,
SilverStripeNavigator: `${PATHS.SRC}/styles/SilverStripeNavigator.scss`,
})
.getConfig(),
},
output: {
path: PATHS.DIST,
filename: 'styles/[name].css',
},
devtool: (ENV !== 'production') ? 'source-map' : '',
module: moduleCSS(ENV, PATHS),
plugins: pluginCSS(ENV, PATHS),
},
];
// Use WEBPACK_CHILD=js or WEBPACK_CHILD=css env var to run a single config
module.exports = (process.env.WEBPACK_CHILD)
? config.find((entry) => entry.name === process.env.WEBPACK_CHILD)
: config;
: module.exports = config;

14730
yarn.lock

File diff suppressed because it is too large Load Diff