API Support versioned File management

API Decouple File and ErrorPage
API Link tracking is now only performed on stage (in lieu of versioned relationships)
API Refactor versioned API methods out of SiteTree and into Versioned
This commit is contained in:
Damian Mooyman 2016-01-26 18:38:42 +13:00
parent c8748f58f3
commit 1c907dd227
21 changed files with 406 additions and 303 deletions

View File

@ -7,3 +7,6 @@ Controller:
Form:
extensions:
- ErrorPageControllerExtension
File:
extensions:
- ErrorPageFileExtension

View File

@ -39,7 +39,7 @@ class CMSBatchAction_Unpublish extends CMSBatchAction {
}
public function applicablePages($ids) {
return $this->applicablePagesHelper($ids, 'canDeleteFromLive', false, true);
return $this->applicablePagesHelper($ids, 'canUnpublish', false, true);
}
}
@ -182,12 +182,15 @@ class CMSBatchAction_DeleteFromLive extends CMSBatchAction {
'modified'=>array(),
'deleted'=>array()
);
/** @var SiteTree $page */
foreach($pages as $page) {
$id = $page->ID;
// Perform the action
if($page->canDelete()) $page->doDeleteFromLive();
if($page->canUnpublish()) {
$page->doUnpublish();
}
// check to see if the record exists on the stage site, if it doesn't remove the tree node
$stageRecord = Versioned::get_one_by_stage( 'SiteTree', 'Stage', array(

View File

@ -63,6 +63,7 @@ class AssetAdmin extends LeftAndMain implements PermissionProvider{
public function init() {
parent::init();
Versioned::reading_stage("Stage");
Requirements::javascript(CMS_DIR . "/javascript/dist/AssetAdmin.js");
Requirements::add_i18n_javascript(CMS_DIR . '/javascript/lang', false, true);

View File

@ -533,7 +533,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
*
* @param int $id Record ID
* @param int $versionID optional Version id of the given record
* @return DataObject
* @return SiteTree
*/
public function getRecord($id, $versionID = null) {
$treeClass = $this->stat('tree_class');
@ -591,7 +591,6 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
* @return CMSForm
*/
public function getEditForm($id = null, $fields = null) {
if(!$id) $id = $this->currentPageID();
$form = parent::getEditForm($id);
@ -604,7 +603,6 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
if($record) {
$deletedFromStage = $record->getIsDeletedFromStage();
$deleteFromLive = !$record->getExistsOnLive();
$fields->push($idField = new HiddenField("ID", false, $id));
// Necessary for different subsites
@ -974,13 +972,15 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
*/
public function deletefromlive($data, $form) {
Versioned::reading_stage('Live');
$record = DataObject::get_by_id("SiteTree", $data['ID']);
if($record && !($record->canDelete() && $record->canDeleteFromLive())) return Security::permissionFailure($this);
$descRemoved = '';
/** @var SiteTree $record */
$record = DataObject::get_by_id("SiteTree", $data['ID']);
if($record && !($record->canDelete() && $record->canUnpublish())) {
return Security::permissionFailure($this);
}
$descendantsRemoved = 0;
$recordTitle = $record->Title;
$recordID = $record->ID;
// before deleting the records, get the descendants of this tree
if($record) {
@ -989,13 +989,14 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// then delete them from the live site too
$descendantsRemoved = 0;
foreach( $descendantIDs as $descID )
/** @var SiteTree $descendant */
if( $descendant = DataObject::get_by_id('SiteTree', $descID) ) {
$descendant->doDeleteFromLive();
$descendant->doUnpublish();
$descendantsRemoved++;
}
// delete the record
$record->doDeleteFromLive();
$record->doUnpublish();
}
Versioned::reading_stage('Stage');
@ -1098,8 +1099,8 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
/**
* Delete this page from both live and stage
*
* @param type $data
* @param type $form
* @param array $data
* @param Form $form
*/
public function archive($data, $form) {
$id = $data['ID'];
@ -1131,10 +1132,15 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
public function unpublish($data, $form) {
$className = $this->stat('tree_class');
/** @var SiteTree $record */
$record = DataObject::get_by_id($className, $data['ID']);
if($record && !$record->canDeleteFromLive()) return Security::permissionFailure($this);
if(!$record || !$record->ID) throw new SS_HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404);
if($record && !$record->canUnpublish()) {
return Security::permissionFailure($this);
}
if(!$record || !$record->ID) {
throw new SS_HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404);
}
$record->doUnpublish();

View File

@ -8,6 +8,13 @@
*/
class ErrorPageControllerExtension extends Extension {
/**
* Used by {@see RequestHandler::httpError}
*
* @param int $statusCode
* @param SS_HTTPRequest $request
* @throws SS_HTTPResponse_Exception
*/
public function onBeforeHTTPError($statusCode, $request) {
$response = ErrorPage::response_for($statusCode);
if($response) {

View File

@ -0,0 +1,18 @@
<?php
/**
* Decorates {@see File} with ErrorPage support
*/
class ErrorPageFileExtension extends DataExtension {
/**
* Used by {@see File::handle_shortcode}
*
* @param int $statusCode HTTP Error code
* @return DataObject Substitute object suitable for handling the given error code
*/
public function getErrorRecordFor($statusCode) {
return ErrorPage::get()->filter("ErrorCode", $statusCode)->first();
}
}

View File

@ -26,7 +26,6 @@
*
* @method ManyManyList ViewerGroups List of groups that can view this object.
* @method ManyManyList EditorGroups List of groups that can edit this object.
* @method ManyManyList BackLinkTracking List of site pages that link to this page.
*
* @mixin Hierarchy
* @mixin Versioned
@ -129,21 +128,10 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
);
private static $many_many = array(
"LinkTracking" => "SiteTree",
"ImageTracking" => "File",
"ViewerGroups" => "Group",
"EditorGroups" => "Group",
);
private static $belongs_many_many = array(
"BackLinkTracking" => "SiteTree"
);
private static $many_many_extraFields = array(
"LinkTracking" => array("FieldName" => "Varchar"),
"ImageTracking" => array("FieldName" => "Varchar")
);
private static $casting = array(
"Breadcrumbs" => "HTMLText",
"LastEdited" => "SS_Datetime",
@ -1080,39 +1068,21 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
}
/**
* This function should return true if the current user can publish this page. It can be overloaded to customise
* the security model for an application.
*
* Denies permission if any of the following conditions is true:
* - canPublish() on any extension returns false
* - canEdit() returns false
*
* @uses SiteTreeExtension->canPublish()
*
* @param Member $member
* @return bool True if the current user can publish this page.
* @deprecated
*/
public function canPublish($member = null) {
if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
if($member && Permission::checkMember($member, "ADMIN")) return true;
// Standard mechanism for accepting permission changes from extensions
$extended = $this->extendedCan('canPublish', $member);
if($extended !== null) return $extended;
// Normal case - fail over to canEdit()
return $this->canEdit($member);
}
public function canDeleteFromLive($member = null) {
// Standard mechanism for accepting permission changes from extensions
$extended = $this->extendedCan('canDeleteFromLive', $member);
if($extended !==null) return $extended;
Deprecation::notice('4.0', 'Use canUnpublish');
return $this->canPublish($member);
// Deprecated extension
$extended = $this->extendedCan('canDeleteFromLive', $member);
if($extended !== null) {
Deprecation::notice('4.0', 'Use canUnpublish in your extension instead');
return $extended;
}
return $this->canUnpublish($member);
}
/**
* Stub method to get the site config, unless the current class can provide an alternate.
*
@ -1551,7 +1521,12 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
$this->migrateVersion($this->Version);
}
}
/**
* Trigger synchronisation of link tracking
*
* {@see SiteTreeLinkTracking::augmentSyncLinkTracking}
*/
public function syncLinkTracking() {
$this->extend('augmentSyncLinkTracking');
}
@ -1737,43 +1712,42 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
}
/**
* Rewrites any linked images on this page.
* Rewrites any linked images on this page without creating a new version record.
* Non-image files should be linked via shortcodes
* Triggers the onRenameLinkedAsset action on extensions.
* TODO: This doesn't work for HTMLText fields on other tables.
*
* @todo Implement image shortcodes and remove this feature
*/
public function rewriteFileLinks() {
// Skip live stage
if(\Versioned::current_stage() === \Versioned::get_live_stage()) {
return;
}
// Update the content without actually creating a new version
foreach(array("SiteTree_Live", "SiteTree") as $table) {
// Published site
$published = DB::prepared_query(
"SELECT * FROM \"$table\" WHERE \"ID\" = ?",
array($this->ID)
)->record();
$origPublished = $published;
foreach($this->db() as $fieldName => $fieldType) {
// Skip if non HTML or if empty
if ($fieldType !== 'HTMLText' || empty($published[$fieldName])) {
continue;
}
// Regenerate content
$content = Image::regenerate_html_links($published[$fieldName]);
if($content === $published[$fieldName]) {
continue;
}
$query = sprintf('UPDATE "%s" SET "%s" = ? WHERE "ID" = ?', $table, $fieldName);
DB::prepared_query($query, array($content, $this->ID));
// Tell static caching to update itself
if($table == 'SiteTree_Live') {
$publishedClass = $origPublished['ClassName'];
$origPublishedObj = new $publishedClass($origPublished);
$this->invokeWithExtensions('onRenameLinkedAsset', $origPublishedObj);
}
foreach($this->db() as $fieldName => $fieldType) {
// Skip if non HTML or if empty
if ($fieldType !== 'HTMLText') {
continue;
}
$fieldValue = $this->{$fieldName};
if(empty($fieldValue)) {
continue;
}
// Regenerate content
$content = Image::regenerate_html_links($fieldValue);
if($content === $fieldValue) {
continue;
}
// Write content directly without updating linked assets
$table = ClassInfo::table_for_object_field($this, $fieldName);
$query = sprintf('UPDATE "%s" SET "%s" = ? WHERE "ID" = ?', $table, $fieldName);
DB::prepared_query($query, array($content, $this->ID));
// Update linked assets
$this->invokeWithExtensions('onRenameLinkedAsset');
}
}
@ -2267,7 +2241,7 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
return $actions;
}
if($this->isPublished() && $this->canPublish() && !$this->getIsDeletedFromStage() && $this->canDeleteFromLive()) {
if($this->isPublished() && $this->canPublish() && !$this->getIsDeletedFromStage() && $this->canUnpublish()) {
// "unpublish"
$moreOptions->push(
FormAction::create('unpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete')
@ -2292,7 +2266,7 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
if($existsOnLive) {
// "restore"
$majorActions->push(FormAction::create('revert',_t('CMSMain.RESTORE','Restore')));
if($this->canDelete() && $this->canDeleteFromLive()) {
if($this->canDelete() && $this->canUnpublish()) {
// "delete from live"
$majorActions->push(
FormAction::create('deletefromlive',_t('CMSMain.DELETEFP','Delete'))
@ -2428,11 +2402,13 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
/**
* Unpublish this page - remove it from the live site
*
* Overrides {@see Versioned::doUnpublish()}
*
* @uses SiteTreeExtension->onBeforeUnpublish()
* @uses SiteTreeExtension->onAfterUnpublish()
*/
public function doUnpublish() {
if(!$this->canDeleteFromLive()) return false;
if(!$this->canUnpublish()) return false;
if(!$this->ID) return false;
$this->invokeWithExtensions('onBeforeUnpublish', $this);
@ -2550,58 +2526,10 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
}
/**
* Removes the page from both live and stage
*
* @return bool Success
*/
public function doArchive() {
$this->invokeWithExtensions('onBeforeArchive', $this);
if($this->doUnpublish()) {
$this->delete();
$this->invokeWithExtensions('onAfterArchive', $this);
return true;
}
return false;
}
/**
* Check if the current user is allowed to archive this page.
* If extended, ensure that both canDelete and canDeleteFromLive are extended also
*
* @param Member $member
* @return bool
*/
public function canArchive($member = null) {
if(!$member) {
$member = Member::currentUser();
}
// Standard mechanism for accepting permission changes from extensions
$extended = $this->extendedCan('canArchive', $member);
if($extended !== null) {
return $extended;
}
// Check if this page can be deleted
if(!$this->canDelete($member)) {
return false;
}
// If published, check if we can delete from live
if($this->ExistsOnLive && !$this->canDeleteFromLive($member)) {
return false;
}
return true;
}
/**
* Synonym of {@link doUnpublish}
* @deprecated
*/
public function doDeleteFromLive() {
Deprecation::notice("4.0", "Use doUnpublish instead");
return $this->doUnpublish();
}
@ -2622,21 +2550,6 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
return stripos($this->ID, 'new') === 0;
}
/**
* Check if this page has been published.
*
* @return bool
*/
public function isPublished() {
if($this->isNew())
return false;
return (DB::prepared_query("SELECT \"ID\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($this->ID))->value())
? true
: false;
}
/**
* Get the class dropdown used in the CMS to change the class of a page. This returns the list of options in the
* dropdown as a Map from class name to singular name. Filters by {@link SiteTree->canCreate()}, as well as
@ -2920,7 +2833,7 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
return $classes;
}
/**
* Compares current draft with live version, and returns true if no draft version of this page exists but the page
* is still published (eg, after triggering "Delete from draft site" in the CMS).
@ -2930,7 +2843,7 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
public function getIsDeletedFromStage() {
if(!$this->ID) return true;
if($this->isNew()) return false;
$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
// Return true for both completely deleted pages and for pages just deleted from stage

View File

@ -1,12 +1,22 @@
<?php
/**
* 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.
*
* @property File $owner
*
* @package cms
* @subpackage model
*/
class SiteTreeFileExtension extends DataExtension {
private static $belongs_many_many = array(
'BackLinkTracking' => 'SiteTree'
'BackLinkTracking' => 'SiteTree.ImageTracking' // {@see SiteTreeLinkTracking}
);
public function updateCMSFields(FieldList $fields) {
@ -16,8 +26,8 @@ class SiteTreeFileExtension extends DataExtension {
_t('AssetTableField.BACKLINKCOUNT', 'Used on:'),
$this->BackLinkTracking()->Count() . ' ' . _t('AssetTableField.PAGES', 'page(s)')
)
->addExtraClass('cms-description-toggle')
->setDescription($this->BackLinkHTMLList()),
->addExtraClass('cms-description-toggle')
->setDescription($this->BackLinkHTMLList()),
'LastEdited'
);
}
@ -32,8 +42,8 @@ class SiteTreeFileExtension extends DataExtension {
'SiteTreeFileExtension.BACKLINK_LIST_DESCRIPTION',
'This list shows all pages where the file has been added through a WYSIWYG editor.'
) . '</em>';
$html .= '<ul>';
$html .= '<ul>';
foreach ($this->BackLinkTracking() as $backLink) {
// Add the page link and CMS link
$html .= sprintf(
@ -44,17 +54,14 @@ class SiteTreeFileExtension extends DataExtension {
_t('SiteTreeFileExtension.EDIT', 'Edit')
);
}
$html .= '</ul>';
return $html .= '</ul>';
return $html;
}
/**
* Extend through {@link updateBackLinkTracking()} in your own {@link Extension}.
*
* @param string|array $filter
* @param string $sort
* @param string $join
* @param string $limit
* @return ManyManyList
*/
public function BackLinkTracking() {
@ -88,32 +95,36 @@ class SiteTreeFileExtension extends DataExtension {
}
/**
* Updates link tracking.
* Updates link tracking in the current stage.
*/
public function onAfterDelete() {
// Skip live stage
if(\Versioned::current_stage() === \Versioned::get_live_stage()) {
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) {
$origStage = Versioned::current_stage();
// This will syncLinkTracking on draft
Versioned::reading_stage('Stage');
// This will syncLinkTracking on the same stage as this file
$brokenPages = DataObject::get('SiteTree')->byIDs($brokenPageIDs);
foreach($brokenPages as $brokenPage) {
$brokenPage->write();
}
// This will syncLinkTracking on published
Versioned::reading_stage('Live');
$liveBrokenPages = DataObject::get('SiteTree')->byIDs($brokenPageIDs);
foreach($liveBrokenPages as $brokenPage) {
$brokenPage->write();
}
Versioned::reading_stage($origStage);
}
}
public function onAfterWrite() {
// Update any database references in the current stage
$this->updateLinks();
}
public function onAfterVersionedPublish() {
// Ensure that ->updateLinks is invoked on the draft record
// after ->doPublish() is invoked.
$this->updateLinks();
}
/**
* Rewrite links to the $old file to now point to the $new file.
@ -121,6 +132,11 @@ class SiteTreeFileExtension extends DataExtension {
* @uses SiteTree->rewriteFileID()
*/
public function updateLinks() {
// Skip live stage
if(\Versioned::current_stage() === \Versioned::get_live_stage()) {
return;
}
if(class_exists('Subsite')) {
Subsite::disable_subsite_filter(true);
}

View File

@ -12,6 +12,11 @@
* 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
* only be enabled for the Stage record.
*
* {@see SiteTreeFileExtension} for the extension applied to {@see File}
*
* @property SiteTree $owner
*
* @property bool $HasBrokenFile
@ -19,6 +24,7 @@
*
* @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.
*/
class SiteTreeLinkTracking extends DataExtension {
@ -38,6 +44,10 @@ class SiteTreeLinkTracking extends DataExtension {
"ImageTracking" => "File"
);
private static $belongs_many_many = array(
"BackLinkTracking" => "SiteTree.LinkTracking"
);
private static $many_many_extraFields = array(
"LinkTracking" => array("FieldName" => "Varchar"),
"ImageTracking" => array("FieldName" => "Varchar")
@ -46,6 +56,8 @@ class SiteTreeLinkTracking extends DataExtension {
/**
* Scrape the content of a field to detect anly links to local SiteTree pages or files
*
* @todo - Replace image tracking with shortcodes
*
* @param string $fieldName The name of the field on {@link @owner} to scrape
*/
public function trackLinksInField($fieldName) {
@ -151,8 +163,15 @@ class SiteTreeLinkTracking extends DataExtension {
/**
* Find HTMLText fields on {@link owner} to scrape for links that need tracking
*
* @todo Support versioned many_many for per-stage page link tracking
*/
public function augmentSyncLinkTracking() {
// Skip live tracking
if(\Versioned::current_stage() == \Versioned::get_live_stage()) {
return;
}
// Reset boolean broken flags
$this->owner->HasBrokenLink = false;
$this->owner->HasBrokenFile = false;
@ -169,7 +188,9 @@ class SiteTreeLinkTracking extends DataExtension {
}
}
foreach($htmlFields as $field) $this->trackLinksInField($field);
foreach($htmlFields as $field) {
$this->trackLinksInField($field);
}
}
}

View File

@ -12,7 +12,7 @@ class VirtualPage extends Page {
public static $virtualFields;
/**
* @var Array Define fields that are not virtual - the virtual page must define these fields themselves.
* @var array Define fields that are not virtual - the virtual page must define these fields themselves.
* Note that anything in {@link self::config()->initially_copied_fields} is implicitly included in this list.
*/
private static $non_virtual_fields = array(
@ -34,7 +34,7 @@ class VirtualPage extends Page {
);
/**
* @var Array Define fields that are initially copied to virtual pages but left modifiable after that.
* @var array Define fields that are initially copied to virtual pages but left modifiable after that.
*/
private static $initially_copied_fields = array(
'ShowInMenus',
@ -126,6 +126,7 @@ class VirtualPage extends Page {
if($this->CopyContentFrom()) {
return $this->CopyContentFrom()->allowedChildren();
}
return array();
}
public function syncLinkTracking() {
@ -138,19 +139,14 @@ class VirtualPage extends Page {
/**
* We can only publish the page if there is a published source page
*
* @param Member $member Member to check
* @return bool
*/
public function canPublish($member = null) {
return $this->isPublishable() && parent::canPublish($member);
}
/**
* Return true if we can delete this page from the live site, which is different from can
* we publish it.
*/
public function canDeleteFromLive($member = null) {
return parent::canPublish($member);
}
/**
* Returns true if is page is publishable by anyone at all
* Return false if the source page isn't published yet.
@ -202,7 +198,7 @@ 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\">"
$link = "<a class=\"cmsEditlink\" href=\"admin/pages/edit/show/$this->CopyContentFromID\">"
. _t('VirtualPage.EditLink', 'edit')
. "</a>";
$msgs[] = _t(
@ -376,6 +372,9 @@ class VirtualPage extends Page {
/**
* Ensure we have an up-to-date version of everything.
*
* @param DataObject $source
* @param bool $updateImageTracking
*/
public function copyFrom($source, $updateImageTracking = true) {
if($source) {

View File

@ -215,6 +215,7 @@ in the other stage:<br />
$removedOrphans = array();
$orphanBaseClass = ClassInfo::baseDataClass($this->orphanedSearchClass);
foreach($orphanIDs as $id) {
/** @var SiteTree $stageRecord */
$stageRecord = Versioned::get_one_by_stage(
$this->orphanedSearchClass,
'Stage',
@ -226,6 +227,7 @@ in the other stage:<br />
$stageRecord->destroy();
unset($stageRecord);
}
/** @var SiteTree $liveRecord */
$liveRecord = Versioned::get_one_by_stage(
$this->orphanedSearchClass,
'Live',
@ -233,7 +235,7 @@ in the other stage:<br />
);
if($liveRecord) {
$removedOrphans[$liveRecord->ID] = sprintf('Removed %s (#%d) from Live', $liveRecord->Title, $liveRecord->ID);
$liveRecord->doDeleteFromLive();
$liveRecord->doUnpublish();
$liveRecord->destroy();
unset($liveRecord);
}

View File

@ -0,0 +1,58 @@
<?php
class ErrorPageFileExtensionTest extends SapphireTest {
protected static $fixture_file = 'ErrorPageTest.yml';
protected $versionedMode = null;
public function setUp() {
parent::setUp();
$this->versionedMode = Versioned::get_reading_mode();
Versioned::reading_stage('Stage');
AssetStoreTest_SpyStore::activate('ErrorPageFileExtensionTest');
$file = new File();
$file->setFromString('dummy', 'dummy.txt');
$file->write();
}
public function tearDown() {
Versioned::set_reading_mode($this->versionedMode);
AssetStoreTest_SpyStore::reset();
parent::tearDown(); // TODO: Change the autogenerated stub
}
public function testErrorPage() {
// Get and publish records
$notFoundPage = $this->objFromFixture('ErrorPage', '404');
$notFoundPage->publish('Stage', 'Live');
$notFoundLink = $notFoundPage->Link();
$disallowedPage = $this->objFromFixture('ErrorPage', '403');
$disallowedPage->publish('Stage', 'Live');
$disallowedLink = $disallowedPage->Link();
// Get stage version of file
$file = File::get()->first();
$fileLink = $file->Link();
Session::clear("loggedInAs");
// Generate shortcode for a file which doesn't exist
$shortcode = File::handle_shortcode(array('id' => 9999), null, new ShortcodeParser(), 'file_link');
$this->assertEquals($notFoundLink, $shortcode);
$shortcode = File::handle_shortcode(array('id' => 9999), 'click here', new ShortcodeParser(), 'file_link');
$this->assertEquals(sprintf('<a href="%s">%s</a>', $notFoundLink, 'click here'), $shortcode);
// Test that user cannot view draft file
$shortcode = File::handle_shortcode(array('id' => $file->ID), null, new ShortcodeParser(), 'file_link');
$this->assertEquals($disallowedLink, $shortcode);
$shortcode = File::handle_shortcode(array('id' => $file->ID), 'click here', new ShortcodeParser(), 'file_link');
$this->assertEquals(sprintf('<a href="%s">%s</a>', $disallowedLink, 'click here'), $shortcode);
// Authenticated users don't get the same error
$this->logInWithPermission('ADMIN');
$shortcode = File::handle_shortcode(array('id' => $file->ID), null, new ShortcodeParser(), 'file_link');
$this->assertEquals($fileLink, $shortcode);
}
}

View File

@ -1,11 +1,11 @@
ErrorPage:
404:
Title: Page Not Found
URLSegment: page-not-found
Content: My error page body
ErrorCode: 404
403:
Title: Permission Failure
URLSegment: permission-denied
Content: You do not have permission to view this page
ErrorCode: 403
404:
Title: Page Not Found
URLSegment: page-not-found
Content: My error page body
ErrorCode: 404
403:
Title: Permission Failure
URLSegment: permission-denied
Content: You do not have permission to view this page
ErrorCode: 403

View File

@ -8,6 +8,9 @@ class FileLinkTrackingTest extends SapphireTest {
public function setUp() {
parent::setUp();
Versioned::reading_stage('Stage');
AssetStoreTest_SpyStore::activate('FileLinkTrackingTest');
$this->logInWithPermission('ADMIN');
@ -17,6 +20,8 @@ class FileLinkTrackingTest extends SapphireTest {
$destPath = AssetStoreTest_SpyStore::getLocalPath($file);
Filesystem::makeFolder(dirname($destPath));
file_put_contents($destPath, str_repeat('x', 1000000));
// Ensure files are published, thus have public urls
$file->doPublish();
}
// Since we can't hard-code IDs, manually inject image tracking shortcode
@ -36,7 +41,13 @@ class FileLinkTrackingTest extends SapphireTest {
public function testFileRenameUpdatesDraftAndPublishedPages() {
$page = $this->objFromFixture('Page', 'page1');
$this->assertTrue($page->doPublish());
$page->doPublish();
// Live and stage pages both have link to public file
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($page->ID))->value()
);
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($page->ID))->value()
@ -45,7 +56,35 @@ class FileLinkTrackingTest extends SapphireTest {
$file = $this->objFromFixture('Image', '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.
$this->assertContains(
'<img src="/assets/55b443b601/renamed-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($page->ID))->value()
);
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($page->ID))->value()
);
// 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->doPublish();
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($page->ID))->value()
);
$this->assertContains(
// Note: Broken link until shortcode-enabled
'<img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($page->ID))->value()
);
// Publishing the page after publishing the asset will resolve any link issues
$page->doPublish();
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($page->ID))->value()
@ -72,11 +111,16 @@ class FileLinkTrackingTest extends SapphireTest {
$file->Name = 'renamed-test-file.jpg';
$file->write();
// Verify that the draft and publish virtual pages both have the corrected link
// Verify that the draft virtual pages have the correct content
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"',
'<img src="/assets/55b443b601/renamed-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($svp->ID))->value()
);
// Publishing both file and page will update the live record
$file->doPublish();
$page->doPublish();
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($svp->ID))->value()
@ -119,12 +163,16 @@ class FileLinkTrackingTest extends SapphireTest {
$file = DataObject::get_by_id('File', $file->ID);
$file->Name = 'renamed-test-file-second-time.jpg';
$file->write();
$file->doPublish();
// Confirm that the correct image is shown in both the draft and live site
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file-second-time.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($page->ID))->value()
);
// Publishing this record also updates live record
$page->doPublish();
$this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file-second-time.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($page->ID))->value()

View File

@ -1,5 +1,8 @@
<?php
/**
* Tests {@see SiteTreeLinkTracking} broken links feature: LinkTracking
*/
class SiteTreeBacklinksTest extends SapphireTest {
protected static $fixture_file = "SiteTreeBacklinksTest.yml";
@ -78,6 +81,8 @@ class SiteTreeBacklinksTest extends SapphireTest {
}
public function testChangingUrlOnLiveSiteRewritesLink() {
$this->markTestSkipped("Test disabled until versioned many_many implemented");
// publish page 1 & 3
$page1 = $this->objFromFixture('Page', 'page1');
$page3 = $this->objFromFixture('Page', 'page3');
@ -109,6 +114,8 @@ class SiteTreeBacklinksTest extends SapphireTest {
}
public function testPublishingPageWithModifiedUrlRewritesLink() {
$this->markTestSkipped("Test disabled until versioned many_many implemented");
// publish page 1 & 3
$page1 = $this->objFromFixture('Page', 'page1');
$page3 = $this->objFromFixture('Page', 'page3');
@ -144,6 +151,8 @@ class SiteTreeBacklinksTest extends SapphireTest {
}
public function testPublishingPageWithModifiedLinksRewritesLinks() {
$this->markTestSkipped("Test disabled until versioned many_many implemented");
// publish page 1 & 3
$page1 = $this->objFromFixture('Page', 'page1');
$page3 = $this->objFromFixture('Page', 'page3');
@ -209,6 +218,9 @@ class SiteTreeBacklinksTest extends SapphireTest {
$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());
// @todo - Implement versioned many_many
$this->markTestSkipped("Test disabled until versioned many_many implemented");
// confirm that published link hasn't
$page2Live = Versioned::get_one_by_stage("Page", "Live", "\"SiteTree\".\"ID\" = $page2->ID");
Versioned::reading_stage('Live');

View File

@ -1,11 +1,26 @@
<?php
/**
* Tests {@see SiteTreeLinkTracking} broken links feature: LinkTracking
*
* @package cms
* @subpackage tests
*/
class SiteTreeBrokenLinksTest extends SapphireTest {
protected static $fixture_file = 'SiteTreeBrokenLinksTest.yml';
public function setUp() {
parent::setUp();
Versioned::reading_stage('Stage');
AssetStoreTest_SpyStore::activate('SiteTreeBrokenLinksTest');
$this->logInWithPermission('ADMIN');
}
public function tearDown() {
AssetStoreTest_SpyStore::reset();
parent::tearDown();
}
public function testBrokenLinksBetweenPages() {
$obj = $this->objFromFixture('Page','content');
@ -62,7 +77,7 @@ class SiteTreeBrokenLinksTest extends SapphireTest {
public function testDeletingFileMarksBackedPagesAsBroken() {
// Test entry
$file = new File();
$file->Filename = 'test-file.pdf';
$file->setFromString('test', 'test-file.txt');
$file->write();
$obj = $this->objFromFixture('Page','content');
@ -83,65 +98,48 @@ class SiteTreeBrokenLinksTest extends SapphireTest {
// Delete the file
$file->delete();
// Confirm that it is marked as broken in both stage and live
// Confirm that it is marked as broken in stage
$obj->flushCache();
$obj = DataObject::get_by_id("SiteTree", $obj->ID);
$this->assertEquals(1, $obj->HasBrokenFile);
// Publishing this page marks it as broken on live too
$obj->doPublish();
$liveObj = Versioned::get_one_by_stage("SiteTree", "Live", "\"SiteTree\".\"ID\" = $obj->ID");
$this->assertEquals(1, $liveObj->HasBrokenFile);
}
public function testDeletingMarksBackLinkedPagesAsBroken() {
$this->logInWithPermission('ADMIN');
// Set up two published pages with a link from content -> about
$linkDest = $this->objFromFixture('Page','about');
$linkDest->doPublish();
$linkSrc = $this->objFromFixture('Page','content');
$linkSrc->Content = "<p><a href=\"[sitetree_link,id=$linkDest->ID]\">about us</a></p>";
$linkSrc->write();
$linkSrc->doPublish();
// Confirm no broken link
$this->assertEquals(0, (int)$linkSrc->HasBrokenLink);
$this->assertEquals(0, DB::query("SELECT \"HasBrokenLink\" FROM \"SiteTree_Live\"
WHERE \"ID\" = $linkSrc->ID")->value());
// Delete page from draft
$linkDestID = $linkDest->ID;
$linkDest->delete();
// Confirm draft has broken link, and published doesn't
// Confirm draft has broken link
$linkSrc->flushCache();
$linkSrc = $this->objFromFixture('Page', 'content');
$this->assertEquals(1, (int)$linkSrc->HasBrokenLink);
$this->assertEquals(0, DB::query("SELECT \"HasBrokenLink\" FROM \"SiteTree_Live\"
WHERE \"ID\" = $linkSrc->ID")->value());
// Delete from live
$linkDest = Versioned::get_one_by_stage("SiteTree", "Live", "\"SiteTree\".\"ID\" = $linkDestID");
$linkDest->doDeleteFromLive();
// Confirm both draft and published have broken link
$linkSrc->flushCache();
$linkSrc = $this->objFromFixture('Page', 'content');
$this->assertEquals(1, (int)$linkSrc->HasBrokenLink);
$this->assertEquals(1, DB::query("SELECT \"HasBrokenLink\" FROM \"SiteTree_Live\"
WHERE \"ID\" = $linkSrc->ID")->value());
}
public function testPublishingSourceBeforeDestHasBrokenLink() {
$this->markTestSkipped("Test disabled until versioned many_many implemented");
$this->logInWithPermission('ADMIN');
// Set up two draft pages with a link from content -> about
$linkDest = $this->objFromFixture('Page','about');
// Ensure that it's not on the published site
$linkDest->doDeleteFromLive();
$linkDest->doUnpublish();
$linkSrc = $this->objFromFixture('Page','content');
$linkSrc->Content = "<p><a href=\"[sitetree_link,id=$linkDest->ID]\">about us</a></p>";
@ -157,6 +155,7 @@ class SiteTreeBrokenLinksTest extends SapphireTest {
}
public function testRestoreFixesBrokenLinks() {
$this->markTestSkipped("Test disabled until versioned many_many implemented");
// Create page and virtual page
$p = new Page();
$p->Title = "source";

View File

@ -1,27 +1,21 @@
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>.'
about:
Title: About
URLSegment: about
Content: 'about us here <a name="yes-anchor-here">about skiplinks here</a>.'
brokenInternalRedirector:
RedirectionType: Internal
Title: RedirectorPageToBrokenInteralPage
LinkToID: 0
workingInternalRedirector:
RedirectionType: Internal
Title: RedirectorPageToBrokenInteralPage
LinkTo: =>Page.content
File:
privacypolicy:
Name: privacypolicy.pdf
Title: privacypolicy.pdf
Filename: assets/privacypolicy.pdf
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>.'
about:
Title: About
URLSegment: about
Content: 'about us here <a name="yes-anchor-here">about skiplinks here</a>.'
brokenInternalRedirector:
RedirectionType: Internal
Title: RedirectorPageToBrokenInteralPage
LinkToID: 0
workingInternalRedirector:
RedirectionType: Internal
Title: RedirectorPageToBrokenInteralPage
LinkTo: =>Page.content
ErrorPage:
404:
Title: Page not Found
ErrorCode: 404
404:
Title: Page not Found
ErrorCode: 404

View File

@ -381,7 +381,7 @@ class SiteTreeTest extends SapphireTest {
$parentPage = $this->objFromFixture('Page', 'about');
$parentPage->doDeleteFromLive();
$parentPage->doUnpublish();
Versioned::reading_stage('Live');
@ -425,7 +425,7 @@ class SiteTreeTest extends SapphireTest {
$pageStaffDuplicate->doPublish();
$parentPage = $this->objFromFixture('Page', 'about');
$parentPage->doDeleteFromLive();
$parentPage->doUnpublish();
Versioned::reading_stage('Live');
$this->assertFalse(DataObject::get_by_id('Page', $pageAbout->ID));

View File

@ -198,14 +198,14 @@ class VirtualPageTest extends FunctionalTest {
// Delete the source page
$this->assertTrue($vp->canPublish());
$this->assertTrue($p->doDeleteFromLive());
$this->assertTrue($p->doUnpublish());
// Confirm that we can unpublish, but not publish
$this->assertTrue($vp->canDeleteFromLive());
$this->assertTrue($vp->canUnpublish());
$this->assertFalse($vp->canPublish());
// Confirm that the action really works
$this->assertTrue($vp->doDeleteFromLive());
$this->assertTrue($vp->doUnpublish());
$this->assertNull(DB::query("SELECT \"ID\" FROM \"SiteTree_Live\" WHERE \"ID\" = $vp->ID")->value());
}
@ -398,7 +398,7 @@ class VirtualPageTest extends FunctionalTest {
// Delete the source page form live, confirm that the virtual page has also been unpublished
$pLive = Versioned::get_one_by_stage('SiteTree', 'Live', '"SiteTree"."ID" = ' . $pID);
$this->assertTrue($pLive->doDeleteFromLive());
$this->assertTrue($pLive->doUnpublish());
$vpLive = Versioned::get_one_by_stage('SiteTree', 'Live', '"SiteTree"."ID" = ' . $vp->ID);
$this->assertNull($vpLive);

View File

@ -225,7 +225,10 @@ class ZZZSearchFormTest extends FunctionalTest {
$sf = new SearchForm($this->mockController, 'SearchForm');
$dontShowInSearchFile = $this->objFromFixture('File', 'dontShowInSearchFile');
$dontShowInSearchFile->publish('Stage', 'Live');
$showInSearchFile = $this->objFromFixture('File', 'showInSearchFile');
$showInSearchFile->publish('Stage', 'Live');
$results = $sf->getResults(null, array('Search'=>'dontShowInSearchFile'));
$this->assertNotContains(
$dontShowInSearchFile->ID,

View File

@ -1,43 +1,43 @@
Group:
websiteusers:
Title: View certain restricted pages
websiteusers:
Title: View certain restricted pages
Member:
randomuser:
Email: randomuser@test.com
Password: test
websiteuser:
Email: websiteuser@test.com
Password: test
Groups: =>Group.websiteusers
randomuser:
Email: randomuser@test.com
Password: test
websiteuser:
Email: websiteuser@test.com
Password: test
Groups: =>Group.websiteusers
SiteTree:
searchformholder:
URLSegment: searchformholder
Title: searchformholder
publicPublishedPage:
Title: publicPublishedPage
publicUnpublishedPage:
Title: publicUnpublishedPage
restrictedViewLoggedInUsers:
CanViewType: LoggedInUsers
Title: restrictedViewLoggedInUsers
restrictedViewOnlyWebsiteUsers:
CanViewType: OnlyTheseUsers
ViewerGroups: =>Group.websiteusers
Title: restrictedViewOnlyWebsiteUsers
inheritRestrictedView:
CanViewType: Inherit
Parent: =>SiteTree.restrictedViewLoggedInUsers
Title: inheritRestrictedView
dontShowInSearchPage:
Title: dontShowInSearchPage
ShowInSearch: 0
pageWithSpecialChars:
Title: Brötchen
Content: Frisch vom B&auml;cker
searchformholder:
URLSegment: searchformholder
Title: searchformholder
publicPublishedPage:
Title: publicPublishedPage
publicUnpublishedPage:
Title: publicUnpublishedPage
restrictedViewLoggedInUsers:
CanViewType: LoggedInUsers
Title: restrictedViewLoggedInUsers
restrictedViewOnlyWebsiteUsers:
CanViewType: OnlyTheseUsers
ViewerGroups: =>Group.websiteusers
Title: restrictedViewOnlyWebsiteUsers
inheritRestrictedView:
CanViewType: Inherit
Parent: =>SiteTree.restrictedViewLoggedInUsers
Title: inheritRestrictedView
dontShowInSearchPage:
Title: dontShowInSearchPage
ShowInSearch: 0
pageWithSpecialChars:
Title: Brötchen
Content: Frisch vom B&auml;cker
File:
showInSearchFile:
Title: showInSearchFile
ShowInSearch: 1
dontShowInSearchFile:
Title: dontShowInSearchFile
ShowInSearch: 0
showInSearchFile:
Title: showInSearchFile
ShowInSearch: 1
dontShowInSearchFile:
Title: dontShowInSearchFile
ShowInSearch: 0