API Implement polymorphic sitetree link tracking (#2123)

* WIP Implement polymorphic sitetree link tracking

* Update unit tests
Merge SiteTreeTrackedPage into SiteTree directly

* Fix bugs and issues

* Fix support for file link tracking

* Add missing use

* Add back deprecated extension

* Remove obsolete belongs_many_many

* Update deprecations

* BUG Ensure non-SiteTree records support link tracking

* Safer changed check

* Shift file tracking test to assets module

* Better check for live stage on versioning

* Deprecate method

* Cleanup virtualpage

* Clear records on delete

* Ensure upgrade task occurs on draft

* fix linting
This commit is contained in:
Damian Mooyman 2018-04-06 15:53:57 +12:00 committed by Aaron Carlino
parent 48f53a522a
commit 6c616f5f7a
24 changed files with 480 additions and 952 deletions

View File

@ -4,9 +4,6 @@ Name: cmsextensions
SilverStripe\Admin\LeftAndMain:
extensions:
- SilverStripe\CMS\Controllers\LeftAndMainPageIconsExtension
SilverStripe\Assets\File:
extensions:
- SilverStripe\CMS\Model\SiteTreeFileExtension
---
Name: cmsmodals
---

6
_config/linktracking.yml Normal file
View File

@ -0,0 +1,6 @@
---
Name: cmslinktracking
---
SilverStripe\ORM\DataObject:
extensions:
- SilverStripe\CMS\Model\SiteTreeLinkTracking

View File

@ -4,6 +4,7 @@ namespace SilverStripe\CMS\Model;
use Page;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Assets\Shortcodes\FileLinkTracking;
use SilverStripe\CampaignAdmin\AddToCampaignHandler_FormAction;
use SilverStripe\CMS\Controllers\CMSPageEditController;
use SilverStripe\CMS\Controllers\ContentController;
@ -48,6 +49,7 @@ use SilverStripe\ORM\CMSPreviewable;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\HasManyList;
use SilverStripe\ORM\HiddenClass;
use SilverStripe\ORM\Hierarchy\Hierarchy;
use SilverStripe\ORM\ManyManyList;
@ -82,25 +84,29 @@ use Subsite;
* {@link AbsoluteLink()}. You can allow these segments to contain multibyte characters through
* {@link URLSegmentFilter::$default_allow_multibyte}.
*
* @property string URLSegment
* @property string Title
* @property string MenuTitle
* @property string Content HTML content of the page.
* @property string MetaDescription
* @property string ExtraMeta
* @property string ShowInMenus
* @property string ShowInSearch
* @property string Sort Integer value denoting the sort order.
* @property string ReportClass
* @property string $URLSegment
* @property string $Title
* @property string $MenuTitle
* @property string $Content HTML content of the page.
* @property string $MetaDescription
* @property string $ExtraMeta
* @property string $ShowInMenus
* @property string $ShowInSearch
* @property string $Sort Integer value denoting the sort order.
* @property string $ReportClass
* @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
* @mixin SiteTreeLinkTracking Added via linktracking.yml to DataObject directly
* @mixin FileLinkTracking Added via filetracking.yml in silverstripe/assets
* @mixin InheritedPermissionsExtension
*/
class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvider, CMSPreviewable, Resettable, Flushable, MemberCacheFlusher
@ -205,9 +211,10 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
"URLSegment" => true,
);
private static $has_many = array(
"VirtualPages" => VirtualPage::class . '.CopyContentFrom'
);
private static $has_many = [
"VirtualPages" => VirtualPage::class . '.CopyContentFrom',
'BackLinks' => SiteTreeLink::class . '.Linked',
];
private static $owned_by = array(
"VirtualPages"
@ -262,7 +269,6 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
private static $extensions = [
Hierarchy::class,
Versioned::class,
SiteTreeLinkTracking::class,
InheritedPermissionsExtension::class,
];
@ -384,7 +390,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
// Grab the initial root level page to traverse down from.
$URLSegment = array_shift($parts);
$conditions = array('"SiteTree"."URLSegment"' => rawurlencode($URLSegment));
if (self::config()->nested_urls) {
if (self::config()->get('nested_urls')) {
$conditions[] = array('"SiteTree"."ParentID"' => 0);
}
/** @var SiteTree $sitetree */
@ -392,7 +398,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
/// Fall back on a unique URLSegment for b/c.
if (!$sitetree
&& self::config()->nested_urls
&& self::config()->get('nested_urls')
&& $sitetree = DataObject::get_one(self::class, array(
'"SiteTree"."URLSegment"' => $URLSegment
), $cache)
@ -402,7 +408,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
// Attempt to grab an alternative page from extensions.
if (!$sitetree) {
$parentID = self::config()->nested_urls ? 0 : null;
$parentID = self::config()->get('nested_urls') ? 0 : null;
if ($alternatives = static::singleton()->extend('alternateGetByLink', $URLSegment, $parentID)) {
foreach ($alternatives as $alternative) {
@ -418,7 +424,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
}
// Check if we have any more URL parts to parse.
if (!self::config()->nested_urls || !count($parts)) {
if (!self::config()->get('nested_urls') || !count($parts)) {
return $sitetree;
}
@ -600,7 +606,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
*/
public function RelativeLink($action = null)
{
if ($this->ParentID && self::config()->nested_urls) {
if ($this->ParentID && self::config()->get('nested_urls')) {
$parent = $this->Parent();
// If page is removed select parent from version history (for archive page view)
if ((!$parent || !$parent->exists()) && !$this->isOnDraft()) {
@ -843,6 +849,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
* @param boolean $unlinked Whether to link page titles.
* @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
* @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
* @param string $delimiter Delimiter character (raw html)
* @return string The breadcrumb trail.
*/
public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false, $delimiter = '»')
@ -913,8 +920,9 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
*/
public function getParent()
{
if ($parentID = $this->getField("ParentID")) {
return DataObject::get_by_id(self::class, $parentID);
$parentID = $this->getField("ParentID");
if ($parentID) {
return SiteTree::get_by_id(self::class, $parentID);
}
return null;
}
@ -1377,14 +1385,14 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
));
}
$tags = implode("\n", $tags);
$tagString = implode("\n", $tags);
if ($this->ExtraMeta) {
$tags .= $this->obj('ExtraMeta')->forTemplate();
$tagString .= $this->obj('ExtraMeta')->forTemplate();
}
$this->extend('MetaTags', $tags);
$this->extend('MetaTags', $tagString);
return $tags;
return $tagString;
}
/**
@ -1411,12 +1419,13 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
parent::requireDefaultRecords();
// default pages
if (static::class == self::class && $this->config()->create_default_pages) {
if (!SiteTree::get_by_link(RootURLController::config()->default_homepage_link)) {
if (static::class === self::class && $this->config()->get('create_default_pages')) {
$defaultHomepage = RootURLController::config()->get('default_homepage_link');
if (!SiteTree::get_by_link($defaultHomepage)) {
$homepage = new Page();
$homepage->Title = _t(__CLASS__.'.DEFAULTHOMETITLE', 'Home');
$homepage->Content = _t(__CLASS__.'.DEFAULTHOMECONTENT', '<p>Welcome to SilverStripe! This is the default homepage. You can edit this page by opening <a href="admin/">the CMS</a>.</p><p>You can now access the <a href="http://docs.silverstripe.org">developer documentation</a>, or begin the <a href="http://www.silverstripe.org/learn/lessons">SilverStripe lessons</a>.</p>');
$homepage->URLSegment = RootURLController::config()->default_homepage_link;
$homepage->URLSegment = $defaultHomepage;
$homepage->Sort = 1;
$homepage->write();
$homepage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
@ -1491,8 +1500,6 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
$count++;
}
$this->syncLinkTracking();
// Check to see if we've only altered fields that shouldn't affect versioning
$fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo', 'VersionID', 'SaveCount');
$changedFields = array_keys($this->getChangedFields(true, 2));
@ -1521,8 +1528,8 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
parent::onBeforeDelete();
// If deleting this page, delete all its children.
if ($this->isInDB() && SiteTree::config()->enforce_strict_hierarchy && $children = $this->AllChildren()) {
foreach ($children as $child) {
if ($this->isInDB() && SiteTree::config()->get('enforce_strict_hierarchy')) {
foreach ($this->AllChildren() as $child) {
/** @var SiteTree $child */
$child->delete();
}
@ -1615,7 +1622,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
public function validURLSegment()
{
// Check known urlsegment blacklists
if (self::config()->nested_urls && $this->ParentID) {
if (self::config()->get('nested_urls') && $this->ParentID) {
// Guard against url segments for sub-pages
$parent = $this->Parent();
if ($controller = ModelAsController::controller_for($parent)) {
@ -1645,7 +1652,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
if ($this->ID) {
$source = $source->exclude('ID', $this->ID);
}
if (self::config()->nested_urls) {
if (self::config()->get('nested_urls')) {
$source = $source->filter('ParentID', $this->ParentID ? $this->ParentID : 0);
}
return !$source->exists();
@ -1685,9 +1692,10 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
*/
public function getStageURLSegment()
{
$stageRecord = Versioned::get_one_by_stage(self::class, Versioned::DRAFT, array(
/** @var SiteTree $stageRecord */
$stageRecord = Versioned::get_one_by_stage(self::class, Versioned::DRAFT, [
'"SiteTree"."ID"' => $this->ID
));
]);
return ($stageRecord) ? $stageRecord->URLSegment : null;
}
@ -1698,17 +1706,37 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
*/
public function getLiveURLSegment()
{
$liveRecord = Versioned::get_one_by_stage(self::class, Versioned::LIVE, array(
/** @var SiteTree $liveRecord */
$liveRecord = Versioned::get_one_by_stage(self::class, Versioned::LIVE, [
'"SiteTree"."ID"' => $this->ID
));
]);
return ($liveRecord) ? $liveRecord->URLSegment : null;
}
/**
* Get the back-link tracking objects that link to this page
*
* @retun ArrayList|DataObject[]
*/
public function BackLinkTracking()
{
// @todo - Implement PolymorphicManyManyList to replace this
$list = ArrayList::create();
foreach ($this->BackLinks() as $link) {
// Ensure parent record exists
$item = $link->Parent();
if ($item && $item->isInDB()) {
$list->push($item);
}
}
return $list;
}
/**
* 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
* @return ArrayList|SiteTree[]
*/
public function DependentPages($includeVirtuals = true)
{
@ -1881,7 +1909,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
$baseLink = Controller::join_links(
Director::absoluteBaseURL(),
(self::config()->nested_urls && $this->ParentID ? $this->Parent()->RelativeLink(true) : null)
(self::config()->get('nested_urls') && $this->ParentID ? $this->Parent()->RelativeLink(true) : null)
);
$urlsegment = SiteTreeURLSegmentField::create("URLSegment", $this->fieldLabel('URLSegment'))
@ -1891,7 +1919,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
'New {pagetype}',
array('pagetype' => $this->i18n_singular_name())
)));
$helpText = (self::config()->nested_urls && $this->numChildren())
$helpText = (self::config()->get('nested_urls') && $this->numChildren())
? $this->fieldLabel('LinkChangeNote')
: '';
if (!Config::inst()->get('SilverStripe\\View\\Parsers\\URLSegmentFilter', 'default_allow_multibyte')) {
@ -2154,14 +2182,14 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
$labels['Comments'] = _t(__CLASS__.'.Comments', 'Comments');
$labels['Visibility'] = _t(__CLASS__.'.Visibility', 'Visibility');
$labels['LinkChangeNote'] = _t(
'SilverStripe\\CMS\\Model\\SiteTree.LINKCHANGENOTE',
__CLASS__ . '.LINKCHANGENOTE',
'Changing this page\'s link will also affect the links of all child pages.'
);
if ($includerelations) {
$labels['Parent'] = _t(__CLASS__.'.has_one_Parent', 'Parent Page', 'The parent page in the site hierarchy');
$labels['LinkTracking'] = _t(__CLASS__.'.many_many_LinkTracking', 'Link Tracking');
$labels['ImageTracking'] = _t(__CLASS__.'.many_many_ImageTracking', 'Image Tracking');
$labels['FileTracking'] = _t(__CLASS__.'.many_many_ImageTracking', 'Image Tracking');
$labels['BackLinkTracking'] = _t(__CLASS__.'.many_many_BackLinkTracking', 'Backlink Tracking');
}
@ -2188,7 +2216,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
// Get status of page
$isOnDraft = $this->isOnDraft();
$isPublished = $this->isPublished();
$stagesDiffer = $this->stagesDiffer(Versioned::DRAFT, Versioned::LIVE);
$stagesDiffer = $this->stagesDiffer();
// Check permissions
$canPublish = $this->canPublish();
@ -2232,6 +2260,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) {
@ -3005,14 +3034,20 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
*/
protected function updateDependentPages()
{
// Skip live stage
if (Versioned::get_stage() === Versioned::LIVE) {
return;
}
// Need to flush cache to avoid outdated versionnumber references
$this->flushCache();
// Need to mark pages depending to this one as broken
$dependentPages = $this->DependentPages();
if ($dependentPages) {
foreach ($dependentPages as $page) {
// $page->write() calls syncLinkTracking, which does all the hard work for us.
/** @var Page $page */
foreach ($this->DependentPages() as $page) {
// Update sync link tracking
$page->syncLinkTracking();
if ($page->isChanged()) {
$page->write();
}
}

View File

@ -3,43 +3,18 @@
namespace SilverStripe\CMS\Model;
use SilverStripe\Assets\File;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\ReadonlyField;
use SilverStripe\ORM\DataExtension;
use SilverStripe\ORM\ManyManyList;
use SilverStripe\Subsites\Model\Subsite;
use SilverStripe\Versioned\Versioned;
use SilverStripe\View\SSViewer;
/**
* Extension applied to {@see File} object to track links to {@see SiteTree} records.
*
* {@see SiteTreeLinkTracking} for the extension applied to {@see SiteTree}
*
* Note that since both SiteTree and File are versioned, LinkTracking and ImageTracking will
* only be enabled for the Stage record.
*
* @deprecated 4.2..5.0 Link tracking is baked into File class now
* @property File $owner
*/
class SiteTreeFileExtension extends DataExtension
{
private static $belongs_many_many = array(
'BackLinkTracking' => SiteTree::class . '.ImageTracking' // {@see SiteTreeLinkTracking}
);
/**
* Images tracked by pages are owned by those pages
*
* @config
* @var array
*/
private static $owned_by = array(
'BackLinkTracking'
);
private static $casting = array(
private static $casting = [
'BackLinkHTMLList' => 'HTMLFragment'
);
];
/**
* Generate an HTML list which provides links to where a file is used.
@ -49,67 +24,6 @@ class SiteTreeFileExtension extends DataExtension
public function BackLinkHTMLList()
{
$viewer = SSViewer::create(["type" => "Includes", self::class . "_description"]);
return $viewer->process($this->owner);
}
/**
* Extend through {@link updateBackLinkTracking()} in your own {@link Extension}.
*
* @return ManyManyList
*/
public function BackLinkTracking()
{
// @todo remove coupling with Subsites
if (class_exists(Subsite::class)) {
$rememberSubsiteFilter = Subsite::$disable_subsite_filter;
Subsite::disable_subsite_filter(true);
}
$links = $this->owner->getManyManyComponents('BackLinkTracking');
$this->owner->extend('updateBackLinkTracking', $links);
if (class_exists(Subsite::class)) {
Subsite::disable_subsite_filter($rememberSubsiteFilter);
}
return $links;
}
/**
* @todo Unnecessary shortcut for AssetTableField, coupled with cms module.
*
* @return int
*/
public function BackLinkTrackingCount()
{
$pages = $this->owner->BackLinkTracking();
if ($pages) {
return $pages->count();
} else {
return 0;
}
}
/**
* Updates link tracking in the current stage.
*/
public function onAfterDelete()
{
// Skip live stage
if (Versioned::get_stage() === Versioned::LIVE) {
return;
}
// We query the explicit ID list, because BackLinkTracking will get modified after the stage
// site does its thing
$brokenPageIDs = $this->owner->BackLinkTracking()->column("ID");
if ($brokenPageIDs) {
// This will syncLinkTracking on the same stage as this file
$brokenPages = SiteTree::get()->byIDs($brokenPageIDs);
foreach ($brokenPages as $brokenPage) {
$brokenPage->write();
}
}
}
}

View File

@ -32,10 +32,8 @@ class SiteTreeFileFormFactoryExtension extends DataExtension
$class = UsedOnTable::class;
Deprecation::notice('5.0', "Use the $class to show this table");
/** @var File|SiteTreeFileExtension|RecursivePublishable $record */
$record = $context['Record'];
$usedOnField = UsedOnTable::create('UsedOnTableReplacement');
$usedOnField->setRecord($context['Record']);
// Add field to new tab
/** @var Tab $tab */

View File

@ -4,26 +4,40 @@ 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;
use SilverStripe\ORM\DB;
/**
* @deprecated 4.2..5.0 Will be removed in cms 5.0
*/
class SiteTreeFolderExtension extends DataExtension
{
public function __construct()
{
parent::__construct();
Deprecation::notice('5.0', 'Will be removed in 5.0');
}
/**
* Looks for files used in system and create where clause which contains all ID's of files.
*
* @deprecated 4.2..5.0
* @returns string where clause which will work as filter.
*/
public function getUnusedFilesListFilter()
{
Deprecation::notice('5.0', 'Will be removed in 5.0');
// Add all records in link tracking
$usedFiles = DB::query("SELECT DISTINCT \"FileID\" FROM \"SiteTree_ImageTracking\"")->column('FileID');
$usedFiles = FileLink::get()->column('LinkedID');
// Get all classes that aren't folder
$fileClasses = array_diff_key(

View File

@ -0,0 +1,23 @@
<?php
namespace SilverStripe\CMS\Model;
use SilverStripe\ORM\DataObject;
/**
* Represents a link between a dataobject parent and a page in a HTML content area
*
* @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
{
private static $table_name = 'SiteTreeLink';
private static $has_one = [
'Parent' => DataObject::class,
'Linked' => SiteTree::class,
];
}

View File

@ -3,11 +3,11 @@
namespace SilverStripe\CMS\Model;
use DOMElement;
use SilverStripe\Assets\File;
use SilverStripe\Assets\Shortcodes\FileLinkTracking;
use SilverStripe\ORM\DataExtension;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\ORM\ManyManyList;
use SilverStripe\ORM\ManyManyThroughList;
use SilverStripe\Versioned\Versioned;
use SilverStripe\View\Parsers\HTMLValue;
@ -18,23 +18,17 @@ use SilverStripe\View\Parsers\HTMLValue;
* referenced in any HTMLText fields, and two booleans to indicate if there are any broken links. Call
* augmentSyncLinkTracking to update those fields with any changes to those fields.
*
* Note that since both SiteTree and File are versioned, LinkTracking and ImageTracking will
* Note that since both SiteTree and File are versioned, LinkTracking and FileTracking will
* only be enabled for the Stage record.
*
* {@see SiteTreeFileExtension} for the extension applied to {@see File}
* Note: To support `HasBrokenLink` for non-SiteTree classes, add a boolean `HasBrokenLink`
* field to your `db` config and this extension will ensure it's flagged appropriately.
*
* @property SiteTree $owner
*
* @property bool $HasBrokenFile
* @property bool $HasBrokenLink
*
* @method ManyManyList LinkTracking() List of site pages linked on this page.
* @method ManyManyList ImageTracking() List of Images linked on this page.
* @method ManyManyList BackLinkTracking List of site pages that link to this page.
* @property DataObject|SiteTreeLinkTracking $owner
* @method ManyManyThroughList LinkTracking() List of site pages linked on this dataobject
*/
class SiteTreeLinkTracking extends DataExtension
{
/**
* @var SiteTreeLinkTracking_Parser
*/
@ -50,6 +44,14 @@ class SiteTreeLinkTracking extends DataExtension
'Parser' => '%$' . SiteTreeLinkTracking_Parser::class
];
private static $many_many = [
"LinkTracking" => [
'through' => SiteTreeLink::class,
'from' => 'Parent',
'to' => 'Linked',
],
];
/**
* Parser for link tracking
*
@ -70,172 +72,126 @@ class SiteTreeLinkTracking extends DataExtension
return $this;
}
private static $db = array(
"HasBrokenFile" => "Boolean",
"HasBrokenLink" => "Boolean"
);
private static $many_many = array(
"LinkTracking" => SiteTree::class,
"ImageTracking" => File::class, // {@see SiteTreeFileExtension}
);
private static $belongs_many_many = array(
"BackLinkTracking" => SiteTree::class . '.LinkTracking',
);
public function onBeforeWrite()
{
// Trigger link tracking (unless this would also be triggered by FileLinkTracking)
if (!$this->owner->hasExtension(FileLinkTracking::class)) {
$this->owner->syncLinkTracking();
}
}
/**
* Tracked images are considered owned by this page
* Public method to call when triggering symlink extension. Can be called externally,
* or overridden by class implementations.
*
* @config
* @var array
* {@see SiteTreeLinkTracking::augmentSyncLinkTracking}
*/
private static $owns = array(
"ImageTracking"
);
public function syncLinkTracking()
{
$this->owner->extend('augmentSyncLinkTracking');
}
private static $many_many_extraFields = array(
"LinkTracking" => array("FieldName" => "Varchar"),
"ImageTracking" => array("FieldName" => "Varchar")
);
/**
* Find HTMLText fields on {@link owner} to scrape for links that need tracking
*/
public function augmentSyncLinkTracking()
{
// If owner is versioned, skip tracking on live
if (Versioned::get_stage() == Versioned::LIVE && $this->owner->hasExtension(Versioned::class)) {
return;
}
// Build a list of HTMLText fields, merging all linked pages together.
$allFields = DataObject::getSchema()->fieldSpecs($this->owner);
$linkedPages = [];
$anyBroken = false;
foreach ($allFields as $field => $fieldSpec) {
$fieldObj = $this->owner->dbObject($field);
if ($fieldObj instanceof DBHTMLText) {
// Merge links in this field with global list.
$linksInField = $this->trackLinksInField($field, $anyBroken);
$linkedPages = array_merge($linkedPages, $linksInField);
}
}
// Soft support for HasBrokenLink db field (e.g. SiteTree)
if ($this->owner->hasField('HasBrokenLink')) {
$this->owner->HasBrokenLink = $anyBroken;
}
// Update the "LinkTracking" many_many.
$this->owner->LinkTracking()->setByIDList($linkedPages);
}
public function onAfterDelete()
{
// If owner is versioned, skip tracking on live
if (Versioned::get_stage() == Versioned::LIVE && $this->owner->hasExtension(Versioned::class)) {
return;
}
$this->owner->LinkTracking()->removeAll();
}
/**
* Scrape the content of a field to detect anly links to local SiteTree pages or files
*
* @param string $fieldName The name of the field on {@link @owner} to scrape
* @param bool &$anyBroken Will be flagged to true (by reference) if a link is broken.
* @return int[] Array of page IDs found (associative array)
*/
public function trackLinksInField($fieldName)
public function trackLinksInField($fieldName, &$anyBroken = false)
{
$record = $this->owner;
// Pull down current field content
$htmlValue = HTMLValue::create($this->owner->$fieldName);
// Process all links
$linkedPages = [];
$linkedFiles = [];
$htmlValue = HTMLValue::create($record->$fieldName);
$links = $this->parser->process($htmlValue);
// Highlight broken links in the content.
foreach ($links as $link) {
// Skip links without domelements
if (!isset($link['DOMReference'])) {
continue;
}
// Toggle highlight class to element
$this->toggleElementClass($link['DOMReference'], 'ss-broken', $link['Broken']);
/** @var DOMElement $domReference */
$domReference = $link['DOMReference'];
$classStr = trim($domReference->getAttribute('class'));
if (!$classStr) {
$classes = [];
} else {
$classes = explode(' ', $classStr);
}
// Add or remove the broken class from the link, depending on the link status.
// Flag broken
if ($link['Broken']) {
$classes = array_unique(array_merge($classes, array('ss-broken')));
} else {
$classes = array_diff($classes, array('ss-broken'));
$anyBroken = true;
}
if (!empty($classes)) {
$domReference->setAttribute('class', implode(' ', $classes));
} else {
$domReference->removeAttribute('class');
}
}
$record->$fieldName = $htmlValue->getContent();
// Populate link tracking for internal links & links to asset files.
foreach ($links as $link) {
switch ($link['Type']) {
case 'sitetree':
if ($link['Broken']) {
$record->HasBrokenLink = true;
} else {
$linkedPages[] = $link['Target'];
}
break;
case 'file':
case 'image':
if ($link['Broken']) {
$record->HasBrokenFile = true;
} else {
$linkedFiles[] = $link['Target'];
}
break;
default:
if ($link['Broken']) {
$record->HasBrokenLink = true;
}
break;
// Collect page ids
if ($link['Type'] === 'sitetree' && $link['Target']) {
$pageID = (int)$link['Target'];
$linkedPages[$pageID] = $pageID;
}
}
// Update the "LinkTracking" many_many
if ($record->getSchema()->manyManyComponent(get_class($record), 'LinkTracking')
&& ($tracker = $record->LinkTracking())
) {
// If already saved, clear existing records
if ($record->isInDB()) {
$tracker->removeByFilter(array(
sprintf('"FieldName" = ? AND "%s" = ?', $tracker->getForeignKey())
=> array($fieldName, $record->ID)
));
}
foreach ($linkedPages as $item) {
$tracker->add($item, array('FieldName' => $fieldName));
}
}
// Update the "ImageTracking" many_many
if ($record->getSchema()->manyManyComponent(get_class($record), 'ImageTracking')
&& ($tracker = $record->ImageTracking())
) {
// If already saved, clear existing records
if ($record->isInDB()) {
$tracker->removeByFilter(array(
sprintf('"FieldName" = ? AND "%s" = ?', $tracker->getForeignKey())
=> array($fieldName, $record->ID)
));
}
foreach ($linkedFiles as $item) {
$tracker->add($item, array('FieldName' => $fieldName));
}
}
// Update any changed content
$this->owner->$fieldName = $htmlValue->getContent();
return $linkedPages;
}
/**
* Find HTMLText fields on {@link owner} to scrape for links that need tracking
* Add the given css class to the DOM element.
*
* @todo Support versioned many_many for per-stage page link tracking
* @param DOMElement $domReference Element to modify.
* @param string $class Class name to toggle.
* @param bool $toggle On or off.
*/
public function augmentSyncLinkTracking()
protected function toggleElementClass(DOMElement $domReference, $class, $toggle)
{
// Skip live tracking
if (Versioned::get_stage() === Versioned::LIVE) {
return;
// Get all existing classes.
$classes = array_filter(explode(' ', trim($domReference->getAttribute('class'))));
// Add or remove the broken class from the link, depending on the link status.
if ($toggle) {
$classes = array_unique(array_merge($classes, [$class]));
} else {
$classes = array_diff($classes, [$class]);
}
// Reset boolean broken flags
$this->owner->HasBrokenLink = false;
$this->owner->HasBrokenFile = false;
// Build a list of HTMLText fields
$allFields = DataObject::getSchema()->fieldSpecs($this->owner);
$htmlFields = array();
foreach ($allFields as $field => $fieldSpec) {
$fieldObj = $this->owner->dbObject($field);
if ($fieldObj instanceof DBHTMLText) {
$htmlFields[] = $field;
}
}
foreach ($htmlFields as $field) {
$this->trackLinksInField($field);
if (!empty($classes)) {
$domReference->setAttribute('class', implode(' ', $classes));
} else {
$domReference->removeAttribute('class');
}
}
}

View File

@ -28,7 +28,6 @@ class SiteTreeLinkTracking_Parser
{
$results = array();
// @todo - Should be calling getElementsByTagName on DOMDocument?
$links = $htmlValue->getElementsByTagName('a');
if (!$links) {
return $results;
@ -61,21 +60,18 @@ class SiteTreeLinkTracking_Parser
// Link to a page on this site.
$matches = array();
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']);
$broken = false;
if (!$page) {
// Page doesn't exist.
$broken = true;
} elseif (!empty($matches['anchor'])) {
// Ensure anchor isn't broken on target page
$anchor = preg_quote($matches['anchor'], '/');
$broken = !preg_match("/(name|id)=\"{$anchor}\"/", $page->Content);
} else {
if (!empty($matches['anchor'])) {
$anchor = preg_quote($matches['anchor'], '/');
if (!preg_match("/(name|id)=\"{$anchor}\"/", $page->Content)) {
// Broken anchor on the target page.
$broken = true;
}
}
$broken = false;
}
$results[] = array(
@ -89,22 +85,7 @@ class SiteTreeLinkTracking_Parser
continue;
}
// Link to a file on this site.
$matches = array();
if (preg_match('/\[file_link(?:\s*|%20|,)?id=(?<id>[0-9]+)/i', $href, $matches)) {
$results[] = array(
'Type' => 'file',
'Target' => $matches['id'],
'Anchor' => null,
'DOMReference' => $link,
'Broken' => !DataObject::get_by_id('SilverStripe\\Assets\\File', $matches['id'])
);
continue;
}
// Local anchor.
$matches = array();
if (preg_match('/^#(.*)/i', $href, $matches)) {
$anchor = preg_quote($matches[1], '#');
$results[] = array(
@ -118,20 +99,6 @@ class SiteTreeLinkTracking_Parser
continue;
}
}
// Find all [image ] shortcodes (will be inline, not inside attributes)
$content = $htmlValue->getContent();
if (preg_match_all('/\[image([^\]]+)\bid=(["])?(?<id>\d+)\D/i', $content, $matches)) {
foreach ($matches['id'] as $id) {
$results[] = array(
'Type' => 'image',
'Target' => (int)$id,
'Anchor' => null,
'DOMReference' => null,
'Broken' => !DataObject::get_by_id('SilverStripe\\Assets\\Image', (int)$id)
);
}
}
return $results;
}
}

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;
@ -12,6 +13,7 @@ use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Member;
use SilverStripe\Versioned\Versioned;
use SilverStripe\View\HTML;
/**
* Virtual Page creates an instance of a page, with the same fields that the original page had, but readonly.
@ -104,9 +106,10 @@ class VirtualPage extends Page
*/
public function getNonVirtualisedFields()
{
$config = self::config();
return array_merge(
self::config()->non_virtual_fields,
self::config()->initially_copied_fields
$config->get('non_virtual_fields'),
$config->get('initially_copied_fields')
);
}
@ -140,8 +143,11 @@ class VirtualPage extends Page
$tags = parent::MetaTags($includeTitle);
$copied = $this->CopyContentFrom();
if ($copied && $copied->exists()) {
$link = Convert::raw2att($copied->Link());
$tags .= "<link rel=\"canonical\" href=\"{$link}\" />\n";
$tags .= HTML::createTag('link', [
'rel' => 'canonical',
'href' => $copied->Link()
]);
$tags .= "\n";
}
return $tags;
}
@ -159,8 +165,8 @@ class VirtualPage extends Page
{
if ($this->CopyContentFromID) {
$this->HasBrokenLink = Versioned::get_by_stage(SiteTree::class, Versioned::DRAFT)
->filter('ID', $this->CopyContentFromID)
->count() === 0;
->filter('ID', $this->CopyContentFromID)
->count() === 0;
} else {
$this->HasBrokenLink = true;
}
@ -212,8 +218,8 @@ class VirtualPage extends Page
// Setup the linking to the original page.
$copyContentFromField = TreeDropdownField::create(
'CopyContentFromID',
_t('SilverStripe\\CMS\\Model\\VirtualPage.CHOOSE', "Linked Page"),
"SilverStripe\\CMS\\Model\\SiteTree"
_t(self::class . '.CHOOSE', "Linked Page"),
SiteTree::class
);
// Setup virtual fields
@ -235,20 +241,24 @@ class VirtualPage extends Page
// Create links back to the original object in the CMS
if ($this->CopyContentFrom()->exists()) {
$link = "<a class=\"cmsEditlink\" href=\"admin/pages/edit/show/$this->CopyContentFromID\">" . _t(
'SilverStripe\\CMS\\Model\\VirtualPage.EditLink',
'edit'
) . "</a>";
$link = HTML::createTag(
'a',
[
'class' => 'cmsEditlink',
'href' => 'admin/pages/edit/show/' . $this->CopyContentFromID,
],
_t(self::class . '.EditLink', 'edit')
);
$msgs[] = _t(
'SilverStripe\\CMS\\Model\\VirtualPage.HEADERWITHLINK',
self::class . '.HEADERWITHLINK',
"This is a virtual page copying content from \"{title}\" ({link})",
array(
[
'title' => $this->CopyContentFrom()->obj('Title'),
'link' => $link,
)
]
);
} else {
$msgs[] = _t('SilverStripe\\CMS\\Model\\VirtualPage.HEADER', "This is a virtual page");
$msgs[] = _t(self::class . '.HEADER', "This is a virtual page");
$msgs[] = _t(
'SilverStripe\\CMS\\Model\\SiteTree.VIRTUALPAGEWARNING',
'Please choose a linked page and save first in order to publish this page'
@ -258,8 +268,7 @@ class VirtualPage extends Page
SiteTree::class,
Versioned::LIVE,
$this->CopyContentFromID
)
) {
)) {
$msgs[] = _t(
'SilverStripe\\CMS\\Model\\SiteTree.VIRTUALPAGEDRAFTWARNING',
'Please publish the linked page in order to publish the virtual page'
@ -295,7 +304,7 @@ class VirtualPage extends Page
// We also want to copy certain, but only if we're copying the source page for the first
// time. After this point, the user is free to customise these for the virtual page themselves.
if ($this->isChanged('CopyContentFromID', 2) && $this->CopyContentFromID) {
foreach (self::config()->initially_copied_fields as $fieldName) {
foreach (self::config()->get('initially_copied_fields') as $fieldName) {
$this->$fieldName = $source->$fieldName;
}
}
@ -337,7 +346,7 @@ class VirtualPage extends Page
if ($orig && $orig->exists() && !$orig->config()->get('can_be_root') && !$this->ParentID) {
$result->addError(
_t(
'SilverStripe\\CMS\\Model\\VirtualPage.PageTypNotAllowedOnRoot',
self::class . '.PageTypNotAllowedOnRoot',
'Original page type "{type}" is not allowed on the root level for this virtual page',
array('type' => $orig->i18n_singular_name())
),
@ -349,10 +358,15 @@ class VirtualPage extends Page
return $result;
}
/**
* @deprecated 4.2..5.0
*/
public function updateImageTracking()
{
Deprecation::notice('5.0', 'This will be removed in 5.0');
// Doesn't work on unsaved records
if (!$this->ID) {
if (!$this->isInDB()) {
return;
}
@ -360,7 +374,12 @@ class VirtualPage extends Page
unset($this->components['CopyContentFrom']);
// Update ImageTracking
$this->ImageTracking()->setByIDList($this->CopyContentFrom()->ImageTracking()->column('ID'));
$copyContentFrom = $this->CopyContentFrom();
if (!$copyContentFrom || !$copyContentFrom->isInDB()) {
return;
}
$this->FileTracking()->setByIDList($copyContentFrom->FileTracking()->column('ID'));
}
public function CMSTreeClasses()

View File

@ -4,58 +4,52 @@ namespace SilverStripe\CMS\Tasks;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Dev\BuildTask;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject;
use SilverStripe\Dev\Debug;
use SilverStripe\ORM\DB;
use SilverStripe\Versioned\Versioned;
/**
* Rewrites plain internal HTML links into shortcode form, using existing link tracking information.
* Updates legacy SiteTree link tracking into new polymorphic many_many relation.
* This should be done for any site upgrading to 4.2.0
*/
class MigrateSiteTreeLinkingTask extends BuildTask
{
private static $segment = 'MigrateSiteTreeLinkingTask';
protected $title = 'Migrate SiteTree Linking Task';
protected $description = 'Rewrites plain internal HTML links into shortcode form, using existing link tracking information.';
protected $description = 'Updates legacy SiteTree link tracking into new polymorphic many_many relation';
public function run($request)
{
$pages = 0;
$links = 0;
$linkedPages = new DataList(SiteTree::class);
$linkedPages = $linkedPages->innerJoin('SiteTree_LinkTracking', '"SiteTree_LinkTracking"."SiteTreeID" = "SiteTree"."ID"');
if ($linkedPages) {
foreach ($linkedPages as $page) {
$tracking = DB::prepared_query(
'SELECT "ChildID", "FieldName" FROM "SiteTree_LinkTracking" WHERE "SiteTreeID" = ?',
array($page->ID)
)->map();
foreach ($tracking as $childID => $fieldName) {
$linked = DataObject::get_by_id(SiteTree::class, $childID);
// TOOD: Replace in all HTMLText fields
$page->Content = preg_replace(
"/href *= *([\"']?){$linked->URLSegment}\/?/i",
"href=$1[sitetree_link,id={$linked->ID}]",
$page->Content,
-1,
$replaced
);
if ($replaced) {
$links += $replaced;
}
}
$page->write();
$pages++;
}
// 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;
}
echo "Rewrote $links link(s) on $pages page(s) to use shortcodes.\n";
$pages = 0;
// Ensure sync occurs on draft
Versioned::withVersionedMode(function () use (&$pages) {
Versioned::set_stage(Versioned::DRAFT);
/** @var SiteTree[] $linkedPages */
$linkedPages = SiteTree::get()
->innerJoin(
'SiteTree_LinkTracking',
'"SiteTree_LinkTracking"."SiteTreeID" = "SiteTree"."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

@ -138,13 +138,13 @@ class ContentControllerTest extends FunctionalTest
$linkedPage = new SiteTree();
$linkedPage->URLSegment = 'linked-page';
$linkedPage->write();
$linkedPage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$linkedPage->publishSingle();
$page = new SiteTree();
$page->URLSegment = 'linking-page';
$page->Content = sprintf('<a href="[sitetree_link,id=%s]">Testlink</a>', $linkedPage->ID);
$page->write();
$page->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$page->publishSingle();
$link = $page->RelativeLink();
$response = $this->get($link);

View File

@ -1,216 +0,0 @@
<?php
namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\Assets\Folder;
use SilverStripe\Assets\Image;
use SilverStripe\Versioned\Versioned;
use SilverStripe\ORM\DataObject;
use SilverStripe\CMS\Model\VirtualPage;
use SilverStripe\Assets\File;
use SilverStripe\Assets\Filesystem;
use SilverStripe\Dev\SapphireTest;
use Silverstripe\Assets\Dev\TestAssetStore;
use Page;
class FileLinkTrackingTest extends SapphireTest
{
protected static $fixture_file = "FileLinkTrackingTest.yml";
public function setUp()
{
parent::setUp();
Versioned::set_stage(Versioned::DRAFT);
TestAssetStore::activate('FileLinkTrackingTest');
$this->logInWithPermission('ADMIN');
// Write file contents
$files = File::get()->exclude('ClassName', Folder::class);
foreach ($files as $file) {
$destPath = TestAssetStore::getLocalPath($file);
Filesystem::makeFolder(dirname($destPath));
file_put_contents($destPath, str_repeat('x', 1000000));
// Ensure files are published, thus have public urls
$file->publishRecursive();
}
// Since we can't hard-code IDs, manually inject image tracking shortcode
$imageID = $this->idFromFixture(Image::class, 'file1');
$page = $this->objFromFixture('Page', 'page1');
$page->Content = sprintf(
'<p>[image src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg" id="%d"]</p>',
$imageID
);
$page->write();
}
public function tearDown()
{
TestAssetStore::reset();
parent::tearDown();
}
/**
* Test uses global state through Versioned::set_reading_mode() since
* the shortcode parser doesn't pass along the underlying DataObject
* context, hence we can't call getSourceQueryParams().
*/
public function testFileRenameUpdatesDraftAndPublishedPages()
{
$page = $this->objFromFixture('Page', 'page1');
$page->publishRecursive();
// Live and stage pages both have link to public file
Versioned::set_stage(Versioned::DRAFT);
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg"',
Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
);
Versioned::set_stage(Versioned::LIVE);
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg"',
Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
);
Versioned::set_stage(Versioned::DRAFT);
$file = $this->objFromFixture(Image::class, 'file1');
$file->Name = 'renamed-test-file.jpg';
$file->write();
// Staged record now points to secure URL of renamed file, live record remains unchanged
// Note that the "secure" url doesn't have the "FileLinkTrackingTest" component because
// the mocked test location disappears for secure files.
Versioned::set_stage(Versioned::DRAFT);
$this->assertContains(
'<img src="/assets/55b443b601/renamed-test-file.jpg"',
Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
);
Versioned::set_stage(Versioned::LIVE);
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg"',
Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
);
// Publishing the file should result in a direct public link (indicated by "FileLinkTrackingTest")
// Although the old live page will still point to the old record.
// @todo - Ensure shortcodes are used with all images to prevent live records having broken links
$file->publishRecursive();
Versioned::set_stage(Versioned::DRAFT);
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"',
Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
);
Versioned::set_stage(Versioned::LIVE);
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"',
Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
);
// Publishing the page after publishing the asset should retain linking
$page->publishRecursive();
Versioned::set_stage(Versioned::DRAFT);
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"',
Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
);
Versioned::set_stage(Versioned::LIVE);
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"',
Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
);
}
public function testFileLinkRewritingOnVirtualPages()
{
// Publish the source page
$page = $this->objFromFixture('Page', 'page1');
$this->assertTrue($page->publishRecursive());
// Create a virtual page from it, and publish that
$svp = new VirtualPage();
$svp->CopyContentFromID = $page->ID;
$svp->write();
$svp->publishRecursive();
// Rename the file
$file = $this->objFromFixture(Image::class, 'file1');
$file->Name = 'renamed-test-file.jpg';
$file->write();
// Verify that the draft virtual pages have the correct content
Versioned::set_stage(Versioned::DRAFT);
$this->assertContains(
'<img src="/assets/55b443b601/renamed-test-file.jpg"',
Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
);
// Publishing both file and page will update the live record
$file->publishRecursive();
$page->publishRecursive();
Versioned::set_stage(Versioned::LIVE);
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"',
Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
);
}
public function testLinkRewritingOnAPublishedPageDoesntMakeItEditedOnDraft()
{
// Publish the source page
/** @var Page $page */
$page = $this->objFromFixture('Page', 'page1');
$this->assertTrue($page->publishRecursive());
$this->assertFalse($page->isModifiedOnDraft());
// Rename the file
$file = $this->objFromFixture(Image::class, 'file1');
$file->Name = 'renamed-test-file.jpg';
$file->write();
// Confirm that the page hasn't gone green.
$this->assertFalse($page->isModifiedOnDraft());
}
public function testTwoFileRenamesInARowWork()
{
$page = $this->objFromFixture('Page', 'page1');
$this->assertTrue($page->publishRecursive());
Versioned::set_stage(Versioned::LIVE);
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg"',
Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
);
// Rename the file twice
Versioned::set_stage(Versioned::DRAFT);
$file = $this->objFromFixture(Image::class, 'file1');
$file->Name = 'renamed-test-file.jpg';
$file->write();
// TODO Workaround for bug in DataObject->getChangedFields(), which returns stale data,
// and influences File->updateFilesystem()
$file = DataObject::get_by_id('SilverStripe\\Assets\\File', $file->ID);
$file->Name = 'renamed-test-file-second-time.jpg';
$file->write();
$file->publishRecursive();
// Confirm that the correct image is shown in both the draft and live site
Versioned::set_stage(Versioned::DRAFT);
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file-second-time.jpg"',
Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
);
// Publishing this record also updates live record
$page->publishRecursive();
Versioned::set_stage(Versioned::LIVE);
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file-second-time.jpg"',
Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
);
}
}

View File

@ -1,12 +0,0 @@
# These need to come first so that SiteTree has the link meta-data written.
SilverStripe\Assets\Image:
file1:
FileFilename: testscript-test-file.jpg
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: testscript-test-file.jpg
Page:
page1:
Title: page1
URLSegment: page1
# Content is set via test setup

View File

@ -2,16 +2,17 @@
namespace SilverStripe\CMS\Tests\Model;
use Page;
use Silverstripe\Assets\Dev\TestAssetStore;
use SilverStripe\CMS\Model\RedirectorPage;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Versioned\Versioned;
use SilverStripe\CMS\Model\SiteTreeLink;
use SilverStripe\CMS\Model\VirtualPage;
use SilverStripe\CMS\Tests\Model\SiteTreeBrokenLinksTest\NotPageObject;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\CMS\Model\VirtualPage;
use SilverStripe\CMS\Model\RedirectorPage;
use SilverStripe\Assets\File;
use SilverStripe\Dev\SapphireTest;
use Silverstripe\Assets\Dev\TestAssetStore;
use Page;
use SilverStripe\Versioned\Versioned;
/**
* Tests {@see SiteTreeLinkTracking} broken links feature: LinkTracking
@ -20,6 +21,10 @@ class SiteTreeBrokenLinksTest extends SapphireTest
{
protected static $fixture_file = 'SiteTreeBrokenLinksTest.yml';
protected static $extra_dataobjects = [
NotPageObject::class,
];
public function setUp()
{
parent::setUp();
@ -44,11 +49,57 @@ class SiteTreeBrokenLinksTest extends SapphireTest
$obj->syncLinkTracking();
$this->assertTrue($obj->HasBrokenLink, 'Page has a broken link');
$obj->Content = '<a href="[sitetree_link,id=' . $this->idFromFixture('Page', 'about') .']">this is not a broken link</a>';
$obj->Content = '<a href="[sitetree_link,id=' . $this->idFromFixture(
'Page',
'about'
) . ']">this is not a broken link</a>';
$obj->syncLinkTracking();
$this->assertFalse($obj->HasBrokenLink, 'Page does NOT have a broken link');
}
/**
* Ensure broken links can be tracked between non-page objects
*/
public function testBrokenLinksNonPage()
{
/** @var Page $aboutPage */
$aboutPage = $this->objFromFixture('Page', 'about');
/** @var NotPageObject $obj */
$obj = $this->objFromFixture(NotPageObject::class, 'object1');
$obj->Content = '<a href="[sitetree_link,id=3423423]">this is a broken link</a>';
$obj->AnotherContent = '<a href="[sitetree_link,id=' . $aboutPage->ID . ']">this is not a broken link</a>';
$obj->write();
// Two links created for this record
$this->assertListEquals(
[
['LinkedID' => 3423423],
['LinkedID' => $aboutPage->ID],
],
SiteTreeLink::get()->filter([
'ParentClass' => NotPageObject::class,
'ParentID' => $obj->ID,
])
);
// ManyManyThrough relation only links to unbroken pages
$this->assertListEquals(
[
['Title' => 'About'],
],
$obj->LinkTracking()
);
// About-page backlinks contains this object
$this->assertListEquals(
[
['ID' => $obj->ID]
],
$aboutPage->BackLinkTracking()
);
}
public function testBrokenAnchorBetweenPages()
{
/** @var Page $obj */
@ -94,47 +145,12 @@ class SiteTreeBrokenLinksTest extends SapphireTest
$this->assertTrue($rp->HasBrokenLink, 'Broken redirector page IS marked as such');
}
public function testDeletingFileMarksBackedPagesAsBroken()
{
// Test entry
$file = new File();
$file->setFromString('test', 'test-file.txt');
$file->write();
/** @var Page $obj */
$obj = $this->objFromFixture('Page', 'content');
$obj->Content = sprintf(
'<p><a href="[file_link,id=%d]">Working Link</a></p>',
$file->ID
);
$obj->write();
$this->assertTrue($obj->publishRecursive());
// Confirm that it isn't marked as broken to begin with
$obj = SiteTree::get()->byID($obj->ID);
$this->assertEquals(0, $obj->HasBrokenFile);
$liveObj = Versioned::get_one_by_stage(SiteTree::class, Versioned::LIVE, "\"SiteTree\".\"ID\" = $obj->ID");
$this->assertEquals(0, $liveObj->HasBrokenFile);
// Delete the file
$file->delete();
// Confirm that it is marked as broken in stage
$obj = SiteTree::get()->byID($obj->ID);
$this->assertEquals(1, $obj->HasBrokenFile);
// Publishing this page marks it as broken on live too
$obj->publishRecursive();
$liveObj = Versioned::get_one_by_stage(SiteTree::class, Versioned::LIVE, "\"SiteTree\".\"ID\" = $obj->ID");
$this->assertEquals(1, $liveObj->HasBrokenFile);
}
public function testDeletingMarksBackLinkedPagesAsBroken()
{
// Set up two published pages with a link from content -> about
$linkDest = $this->objFromFixture('Page', 'about');
/** @var Page $linkSrc */
$linkSrc = $this->objFromFixture('Page', 'content');
$linkSrc->Content = "<p><a href=\"[sitetree_link,id=$linkDest->ID]\">about us</a></p>";
$linkSrc->write();
@ -207,7 +223,9 @@ class SiteTreeBrokenLinksTest extends SapphireTest
$this->assertFalse($rp->HasBrokenLink);
// Unpublishing doesn't affect broken state on live (draft is source of truth)
/** @var SiteTree $p2Live */
$p2Live = Versioned::get_by_stage(SiteTree::class, Versioned::LIVE)->byID($p2->ID);
/** @var SiteTree $rpLive */
$rpLive = Versioned::get_by_stage(SiteTree::class, Versioned::LIVE)->byID($rp->ID);
$this->assertEquals(0, $p2Live->HasBrokenLink);
$this->assertEquals(0, $rpLive->HasBrokenLink);
@ -215,8 +233,10 @@ class SiteTreeBrokenLinksTest extends SapphireTest
// Delete the source page, confirm that the VP, RP and page 2 have broken links on draft
$p->delete();
$p2->flushCache();
/** @var SiteTree $p2 */
$p2 = DataObject::get_by_id(SiteTree::class, $p2->ID);
$rp->flushCache();
/** @var RedirectorPage $rp */
$rp = DataObject::get_by_id(SiteTree::class, $rp->ID);
$this->assertEquals(1, $p2->HasBrokenLink);
$this->assertEquals(1, $rp->HasBrokenLink);
@ -260,7 +280,7 @@ class SiteTreeBrokenLinksTest extends SapphireTest
// Redirector links are a third
$redirectorPage = new RedirectorPage();
$redirectorPage->Title = "redirector";
$redirectorPage->LinkType = 'Internal';
$redirectorPage->RedirectionType = 'Internal';
$redirectorPage->LinkToID = $page->ID;
$redirectorPage->write();
$this->assertTrue($redirectorPage->publishRecursive());
@ -273,8 +293,10 @@ class SiteTreeBrokenLinksTest extends SapphireTest
$page->delete();
$page2->flushCache();
/** @var SiteTree $page2 */
$page2 = DataObject::get_by_id(SiteTree::class, $page2->ID);
$redirectorPage->flushCache();
/** @var RedirectorPage $redirectorPage */
$redirectorPage = DataObject::get_by_id(SiteTree::class, $redirectorPage->ID);
$this->assertEquals(1, $page2->HasBrokenLink);
$this->assertEquals(1, $redirectorPage->HasBrokenLink);

View File

@ -14,3 +14,6 @@ Page:
RedirectionType: Internal
Title: RedirectorPageToBrokenInteralPage
LinkTo: =>Page.content
SilverStripe\CMS\Tests\Model\SiteTreeBrokenLinksTest\NotPageObject:
object1:
Content: 'Everything will be ok'

View File

@ -0,0 +1,22 @@
<?php
namespace SilverStripe\CMS\Tests\Model\SiteTreeBrokenLinksTest;
use SilverStripe\CMS\Model\SiteTreeLinkTracking;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
/**
* @mixin SiteTreeLinkTracking
* @property string $Content
* @property string $AnotherContent
*/
class NotPageObject extends DataObject implements TestOnly
{
private static $table_name = 'SiteTreeLinkTrackingTest_NotPageObject';
private static $db = [
'Content' => 'HTMLText',
'AnotherContent' => 'HTMLText',
];
}

View File

@ -1,62 +0,0 @@
<?php
namespace SilverStripe\CMS\Tests\Model;
use SilverStripe\Assets\File;
use SilverStripe\Assets\Image;
use Silverstripe\Assets\Dev\TestAssetStore;
use SilverStripe\CMS\Tests\Model\SiteTreeFolderExtensionTest\PageWithFile;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Versioned\Versioned;
class SiteTreeFolderExtensionTest extends SapphireTest
{
protected static $extra_dataobjects = [
PageWithFile::class,
];
protected static $fixture_file = 'SiteTreeFolderExtensionTest.yml';
public function setUp()
{
parent::setUp();
Versioned::set_stage(Versioned::DRAFT);
TestAssetStore::activate('SiteTreeFolderExtensionTest');
$this->logInWithPermission('ADMIN');
// Since we can't hard-code IDs, manually inject image tracking shortcode
$imageID = $this->idFromFixture(Image::class, 'image1');
$page = $this->objFromFixture(PageWithFile::class, 'page1');
$page->Content = sprintf(
'<p>[image id="%d"]</p>',
$imageID
);
$page->write();
}
public function tearDown()
{
TestAssetStore::reset();
parent::tearDown();
}
public function testFindsFiles()
{
/** @var PageWithFile $page */
$page = $this->objFromFixture(PageWithFile::class, 'page1');
$query = $page->getUnusedFilesListFilter();
$this->assertContains('"ID" NOT IN', $query);
$this->assertContains('"ClassName" IN (', $query);
$files = File::get()->where($query);
$this->assertDOSEquals(
[
['Name' => 'file2.txt'],
['Name' => 'image2.jpg'],
],
$files
);
}
}

View File

@ -1,28 +0,0 @@
SilverStripe\Assets\Folder:
folder1:
Name: myfolder
folder2:
Name: other
SilverStripe\Assets\File:
file1:
Name: file1.txt
FileFilename: myfolder/file1.txt
Parent: =>SilverStripe\Assets\Folder.folder1
file2:
Name: file2.txt
FileFilename: myfolder/file2.txt
Parent: =>SilverStripe\Assets\Folder.folder1
SilverStripe\Assets\Image:
image1:
Name: image1.jpg
FileFilename: other/image1.jpg
Parent: =>SilverStripe\Assets\Folder.folder2
image2:
Name: image2.jpg
FileFilename: other/image2.jpg
Parent: =>SilverStripe\Assets\Folder.folder2
SilverStripe\CMS\Tests\Model\SiteTreeFolderExtensionTest\PageWithFile:
page1:
Title: mypage
URLSegment: mypage
LinkedFile: =>SilverStripe\Assets\File.file1

View File

@ -1,24 +0,0 @@
<?php
namespace SilverStripe\CMS\Tests\Model\SiteTreeFolderExtensionTest;
use Page;
use SilverStripe\Assets\File;
use SilverStripe\CMS\Model\SiteTreeFolderExtension;
use SilverStripe\Dev\TestOnly;
/**
* @mixin SiteTreeFolderExtension
*/
class PageWithFile extends Page implements TestOnly
{
private static $table_name = 'SiteTreeFolderExtensionTest_PageWithFile';
private static $has_one = [
'LinkedFile' => File::class,
];
private static $extensions = [
SiteTreeFolderExtension::class,
];
}

View File

@ -3,11 +3,10 @@
namespace SilverStripe\CMS\Tests\Model;
use Page;
use Silverstripe\Assets\Dev\TestAssetStore;
use SilverStripe\Assets\Dev\TestAssetStore;
use SilverStripe\Assets\File;
use SilverStripe\Assets\Filesystem;
use SilverStripe\Assets\Folder;
use SilverStripe\Assets\Image;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Dev\CSSContentParser;
use SilverStripe\Dev\FunctionalTest;
@ -46,6 +45,7 @@ class SiteTreeHTMLEditorFieldTest extends FunctionalTest
public function testLinkTracking()
{
/** @var SiteTree $sitetree */
$sitetree = $this->objFromFixture(SiteTree::class, 'home');
$editor = new HTMLEditorField('Content');
@ -84,43 +84,6 @@ class SiteTreeHTMLEditorFieldTest extends FunctionalTest
);
}
public function testFileLinkTracking()
{
$sitetree = $this->objFromFixture(SiteTree::class, 'home');
$editor = new HTMLEditorField('Content');
$fileID = $this->idFromFixture(File::class, 'example_file');
$editor->setValue(sprintf(
'<p><a href="[file_link,id=%d]">Example File</a></p>',
$fileID
));
$editor->saveInto($sitetree);
$sitetree->write();
$this->assertEquals(
array($fileID => $fileID),
$sitetree->ImageTracking()->getIDList(),
'Links to assets are tracked.'
);
$editor->setValue(null);
$editor->saveInto($sitetree);
$sitetree->write();
$this->assertEquals(array(), $sitetree->ImageTracking()->getIdList(), 'Asset tracking is removed with links.');
// Legacy support - old CMS versions added link shortcodes with spaces instead of commas
$editor->setValue(sprintf(
'<p><a href="[file_link id=%d]">Example File</a></p>',
$fileID
));
$editor->saveInto($sitetree);
$sitetree->write();
$this->assertEquals(
array($fileID => $fileID),
$sitetree->ImageTracking()->getIDList(),
'Link tracking with space instead of comma in shortcode works.'
);
}
public function testImageInsertion()
{
$sitetree = new SiteTree();
@ -145,30 +108,6 @@ class SiteTreeHTMLEditorFieldTest extends FunctionalTest
$this->assertEquals('bar', (string)$xml[0]['title'], 'Title tags are preserved.');
}
public function testImageTracking()
{
$sitetree = $this->objFromFixture(SiteTree::class, 'home');
$editor = new HTMLEditorField('Content');
$file = $this->objFromFixture(Image::class, 'example_image');
$editor->setValue(sprintf('[image src="%s" id="%d"]', $file->getURL(), $file->ID));
$editor->saveInto($sitetree);
$sitetree->write();
$this->assertEquals(
array($file->ID => $file->ID),
$sitetree->ImageTracking()->getIDList(),
'Inserted images are tracked.'
);
$editor->setValue(null);
$editor->saveInto($sitetree);
$sitetree->write();
$this->assertEmpty(
$sitetree->ImageTracking()->getIDList(),
'Tracked images are deleted when removed.'
);
}
public function testBrokenSiteTreeLinkTracking()
{
$sitetree = new SiteTree();
@ -192,28 +131,4 @@ class SiteTreeHTMLEditorFieldTest extends FunctionalTest
$this->assertFalse((bool) $sitetree->HasBrokenLink);
}
public function testBrokenFileLinkTracking()
{
$sitetree = new SiteTree();
$editor = new HTMLEditorField('Content');
$this->assertFalse((bool) $sitetree->HasBrokenFile);
$editor->setValue('<p><a href="[file_link,id=0]">Broken Link</a></p>');
$editor->saveInto($sitetree);
$sitetree->write();
$this->assertTrue($sitetree->HasBrokenFile);
$editor->setValue(sprintf(
'<p><a href="[file_link,id=%d]">Working Link</a></p>',
$this->idFromFixture(File::class, 'example_file')
));
$sitetree->HasBrokenFile = false;
$editor->saveInto($sitetree);
$sitetree->write();
$this->assertFalse((bool) $sitetree->HasBrokenFile);
}
}

View File

@ -2,15 +2,15 @@
namespace SilverStripe\CMS\Tests\Model;
use Page;
use SilverStripe\CMS\Model\SiteTreeLinkTracking_Parser;
use SilverStripe\Assets\File;
use SilverStripe\Control\Director;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\View\Parsers\HTMLValue;
use Page;
class SiteTreeLinkTrackingTest extends SapphireTest
{
protected function setUp()
{
parent::setUp();
@ -34,7 +34,6 @@ class SiteTreeLinkTrackingTest extends SapphireTest
// Shortcodes
$this->assertTrue($this->isBroken('<a href="[sitetree_link,id=123]">link</a>'));
$this->assertTrue($this->isBroken('<a href="[sitetree_link,id=123]#no-such-anchor">link</a>'));
$this->assertTrue($this->isBroken('<a href="[file_link,id=123]">link</a>'));
// Relative urls
$this->assertTrue($this->isBroken('<a href="">link</a>'));
@ -57,13 +56,9 @@ class SiteTreeLinkTrackingTest extends SapphireTest
$page->Content = '<a name="yes-name-anchor">name</a><a id="yes-id-anchor">id</a>';
$page->write();
$file = new File();
$file->write();
$this->assertFalse($this->isBroken("<a href=\"[sitetree_link,id=$page->ID]\">link</a>"));
$this->assertFalse($this->isBroken("<a href=\"[sitetree_link,id=$page->ID]#yes-name-anchor\">link</a>"));
$this->assertFalse($this->isBroken("<a href=\"[sitetree_link,id=$page->ID]#yes-id-anchor\">link</a>"));
$this->assertFalse($this->isBroken("<a href=\"[file_link,id=$file->ID]\">link</a>"));
$this->assertTrue($this->isBroken("<a href=\"[sitetree_link,id=$page->ID]#http://invalid-anchor.com\"></a>"));
}
@ -94,18 +89,4 @@ class SiteTreeLinkTrackingTest extends SapphireTest
$this->assertEquals(substr_count($content, 'ss-broken'), 0, 'All ss-broken classes are removed from good link');
$this->assertEquals(substr_count($content, 'existing-class'), 1, 'Existing class is not removed.');
}
public function testHasBrokenFile()
{
$this->assertTrue($this->pageIsBrokenFile('[image src="someurl.jpg" id="99999999"]'));
$this->assertFalse($this->pageIsBrokenFile('[image src="someurl.jpg"]'));
}
protected function pageIsBrokenFile($content)
{
$page = new Page();
$page->Content = $content;
$page->write();
return $page->HasBrokenFile;
}
}

View File

@ -6,6 +6,7 @@ use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\CMS\Tasks\MigrateSiteTreeLinkingTask;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
class MigrateSiteTreeLinkingTaskTest extends SapphireTest
{
@ -13,73 +14,76 @@ class MigrateSiteTreeLinkingTaskTest extends SapphireTest
protected static $use_draft_site = true;
public static function setUpBeforeClass()
{
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()
{
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->assertEquals(
"Rewrote 9 link(s) on 5 page(s) to use shortcodes.\n",
$this->assertContains(
"Migrated page links on 5 Pages",
ob_get_contents(),
'Rewritten links are correctly reported'
);
DB::quiet(true);
ob_end_clean();
$homeID = $this->idFromFixture(SiteTree::class, 'home');
$aboutID = $this->idFromFixture(SiteTree::class, 'about');
$staffID = $this->idFromFixture(SiteTree::class, 'staff');
$actionID = $this->idFromFixture(SiteTree::class, 'action');
$hashID = $this->idFromFixture(SiteTree::class, 'hash_link');
// 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');
$homeContent = sprintf(
'<a href="[sitetree_link,id=%d]">About</a><a href="[sitetree_link,id=%d]">Staff</a><a href="http://silverstripe.org/">External Link</a><a name="anchor"></a>',
$aboutID,
$staffID
);
$aboutContent = sprintf(
'<a href="[sitetree_link,id=%d]">Home</a><a href="[sitetree_link,id=%d]">Staff</a><a name="second-anchor"></a>',
$homeID,
$staffID
);
$staffContent = sprintf(
'<a href="[sitetree_link,id=%d]">Home</a><a href="[sitetree_link,id=%d]">About</a>',
$homeID,
$aboutID
);
$actionContent = sprintf(
'<a href="[sitetree_link,id=%d]SearchForm">Search Form</a>',
$homeID
);
$hashLinkContent = sprintf(
'<a href="[sitetree_link,id=%d]#anchor">Home</a><a href="[sitetree_link,id=%d]#second-anchor">About</a>',
$homeID,
$aboutID
);
$this->assertEquals(
$homeContent,
DataObject::get_by_id(SiteTree::class, $homeID)->Content,
'HTML URLSegment links are rewritten.'
);
$this->assertEquals(
$aboutContent,
DataObject::get_by_id(SiteTree::class, $aboutID)->Content
);
$this->assertEquals(
$staffContent,
DataObject::get_by_id(SiteTree::class, $staffID)->Content
);
$this->assertEquals(
$actionContent,
DataObject::get_by_id(SiteTree::class, $actionID)->Content,
'Links to actions on pages are rewritten correctly.'
);
$this->assertEquals(
$hashLinkContent,
DataObject::get_by_id(SiteTree::class, $hashID)->Content,
'Hash/anchor links are correctly handled.'
);
// Ensure all links are created
$this->assertListEquals([$about->toMap(), $staff->toMap()], $home->LinkTracking());
$this->assertListEquals([$home->toMap(), $staff->toMap()], $about->LinkTracking());
$this->assertListEquals([$home->toMap(), $about->toMap()], $staff->LinkTracking());
$this->assertListEquals([$home->toMap()], $action->LinkTracking());
$this->assertListEquals([$home->toMap(), $about->toMap()], $hash->LinkTracking());
}
}

View File

@ -2,24 +2,24 @@ SilverStripe\CMS\Model\SiteTree:
home:
Title: Home Page
URLSegment: home
Content: '<a href="about/">About</a><a href="staff">Staff</a><a href="http://silverstripe.org/">External Link</a><a name="anchor"></a>'
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="home">Home</a><a href="staff/">Staff</a><a name="second-anchor"></a>'
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="home/">Home</a><a href="about">About</a>'
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="home/SearchForm">Search Form</a>'
Content: '<a href="[sitetree_link,id=$$HOMEID$$]SearchForm">Search Form</a>'
hash_link:
Title: Hash Link
URLSegment: hash-link
Content: '<a href="home/#anchor">Home</a><a href="about/#second-anchor">About</a>'
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