diff --git a/_config/config.yml b/_config/config.yml
index e0e480ff..03c711e4 100644
--- a/_config/config.yml
+++ b/_config/config.yml
@@ -7,3 +7,6 @@ Controller:
Form:
extensions:
- ErrorPageControllerExtension
+File:
+ extensions:
+ - ErrorPageFileExtension
diff --git a/code/batchactions/CMSBatchActions.php b/code/batchactions/CMSBatchActions.php
index 9f10e2b6..fe38fa24 100644
--- a/code/batchactions/CMSBatchActions.php
+++ b/code/batchactions/CMSBatchActions.php
@@ -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(
diff --git a/code/controllers/AssetAdmin.php b/code/controllers/AssetAdmin.php
index b627c05c..d4d7fb84 100644
--- a/code/controllers/AssetAdmin.php
+++ b/code/controllers/AssetAdmin.php
@@ -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);
diff --git a/code/controllers/CMSMain.php b/code/controllers/CMSMain.php
index 15e501a4..420e30f4 100644
--- a/code/controllers/CMSMain.php
+++ b/code/controllers/CMSMain.php
@@ -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();
diff --git a/code/controllers/ErrorPageControllerExtension.php b/code/controllers/ErrorPageControllerExtension.php
index 9fc66cd3..a82ba780 100644
--- a/code/controllers/ErrorPageControllerExtension.php
+++ b/code/controllers/ErrorPageControllerExtension.php
@@ -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) {
diff --git a/code/controllers/ErrorPageFileExtension.php b/code/controllers/ErrorPageFileExtension.php
new file mode 100644
index 00000000..574dface
--- /dev/null
+++ b/code/controllers/ErrorPageFileExtension.php
@@ -0,0 +1,18 @@
+filter("ErrorCode", $statusCode)->first();
+ }
+
+}
diff --git a/code/model/SiteTree.php b/code/model/SiteTree.php
index 1346eac7..15bde52d 100755
--- a/code/model/SiteTree.php
+++ b/code/model/SiteTree.php
@@ -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
diff --git a/code/model/SiteTreeFileExtension.php b/code/model/SiteTreeFileExtension.php
index 57b5777b..d2e71904 100644
--- a/code/model/SiteTreeFileExtension.php
+++ b/code/model/SiteTreeFileExtension.php
@@ -1,12 +1,22 @@
'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.'
) . '';
- $html .= '
';
+ $html .= '';
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 .= '
';
- return $html .= '
';
+ 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);
}
diff --git a/code/model/SiteTreeLinkTracking.php b/code/model/SiteTreeLinkTracking.php
index 2195173c..9f9d16ce 100644
--- a/code/model/SiteTreeLinkTracking.php
+++ b/code/model/SiteTreeLinkTracking.php
@@ -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);
+ }
}
}
diff --git a/code/model/VirtualPage.php b/code/model/VirtualPage.php
index 0576c086..6b85f5a3 100644
--- a/code/model/VirtualPage.php
+++ b/code/model/VirtualPage.php
@@ -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 = "CopyContentFromID\">"
+ $link = "CopyContentFromID\">"
. _t('VirtualPage.EditLink', 'edit')
. "";
$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) {
diff --git a/tasks/RemoveOrphanedPagesTask.php b/tasks/RemoveOrphanedPagesTask.php
index 313e710d..924ab0b9 100644
--- a/tasks/RemoveOrphanedPagesTask.php
+++ b/tasks/RemoveOrphanedPagesTask.php
@@ -215,6 +215,7 @@ in the other stage:
$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:
$stageRecord->destroy();
unset($stageRecord);
}
+ /** @var SiteTree $liveRecord */
$liveRecord = Versioned::get_one_by_stage(
$this->orphanedSearchClass,
'Live',
@@ -233,7 +235,7 @@ in the other stage:
);
if($liveRecord) {
$removedOrphans[$liveRecord->ID] = sprintf('Removed %s (#%d) from Live', $liveRecord->Title, $liveRecord->ID);
- $liveRecord->doDeleteFromLive();
+ $liveRecord->doUnpublish();
$liveRecord->destroy();
unset($liveRecord);
}
diff --git a/tests/model/ErrorPageFileExtensionTest.php b/tests/model/ErrorPageFileExtensionTest.php
new file mode 100644
index 00000000..73e7cd06
--- /dev/null
+++ b/tests/model/ErrorPageFileExtensionTest.php
@@ -0,0 +1,58 @@
+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('%s', $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('%s', $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);
+ }
+
+}
diff --git a/tests/model/ErrorPageTest.yml b/tests/model/ErrorPageTest.yml
index e2152199..b716c73d 100644
--- a/tests/model/ErrorPageTest.yml
+++ b/tests/model/ErrorPageTest.yml
@@ -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
\ No newline at end of file
+ 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
diff --git a/tests/model/FileLinkTrackingTest.php b/tests/model/FileLinkTrackingTest.php
index 22296bdb..371034e5 100644
--- a/tests/model/FileLinkTrackingTest.php
+++ b/tests/model/FileLinkTrackingTest.php
@@ -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(
+ 'ID))->value()
+ );
$this->assertContains(
'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(
+ 'ID))->value()
+ );
+ $this->assertContains(
+ '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(
+ 'ID))->value()
+ );
+ $this->assertContains(
+ // Note: Broken link until shortcode-enabled
+ 'ID))->value()
+ );
+
+ // Publishing the page after publishing the asset will resolve any link issues
+ $page->doPublish();
$this->assertContains(
'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(
- 'ID))->value()
);
+
+ // Publishing both file and page will update the live record
+ $file->doPublish();
+ $page->doPublish();
+
$this->assertContains(
'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(
'ID))->value()
);
+
+ // Publishing this record also updates live record
+ $page->doPublish();
$this->assertContains(
'ID))->value()
diff --git a/tests/model/SiteTreeBacklinksTest.php b/tests/model/SiteTreeBacklinksTest.php
index c64b2678..d5a49ba1 100644
--- a/tests/model/SiteTreeBacklinksTest.php
+++ b/tests/model/SiteTreeBacklinksTest.php
@@ -1,5 +1,8 @@
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('Testing page 1 link
', $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');
diff --git a/tests/model/SiteTreeBrokenLinksTest.php b/tests/model/SiteTreeBrokenLinksTest.php
index 7a4b1b28..d786525a 100644
--- a/tests/model/SiteTreeBrokenLinksTest.php
+++ b/tests/model/SiteTreeBrokenLinksTest.php
@@ -1,11 +1,26 @@
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 = "ID]\">about us
";
$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 = "ID]\">about us
";
@@ -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";
diff --git a/tests/model/SiteTreeBrokenLinksTest.yml b/tests/model/SiteTreeBrokenLinksTest.yml
index 0245ca70..46e7e829 100644
--- a/tests/model/SiteTreeBrokenLinksTest.yml
+++ b/tests/model/SiteTreeBrokenLinksTest.yml
@@ -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 skiplink here.'
- about:
- Title: About
- URLSegment: about
- Content: 'about us here about skiplinks here.'
- 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 skiplink here.'
+ about:
+ Title: About
+ URLSegment: about
+ Content: 'about us here about skiplinks here.'
+ 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
diff --git a/tests/model/SiteTreeTest.php b/tests/model/SiteTreeTest.php
index 01962240..55b85873 100644
--- a/tests/model/SiteTreeTest.php
+++ b/tests/model/SiteTreeTest.php
@@ -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));
diff --git a/tests/model/VirtualPageTest.php b/tests/model/VirtualPageTest.php
index 0e8aece3..da33d251 100644
--- a/tests/model/VirtualPageTest.php
+++ b/tests/model/VirtualPageTest.php
@@ -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);
diff --git a/tests/search/SearchFormTest.php b/tests/search/SearchFormTest.php
index 5b8dfa0f..8313a800 100644
--- a/tests/search/SearchFormTest.php
+++ b/tests/search/SearchFormTest.php
@@ -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,
diff --git a/tests/search/SearchFormTest.yml b/tests/search/SearchFormTest.yml
index 6b1cbc0a..1cc8a685 100644
--- a/tests/search/SearchFormTest.yml
+++ b/tests/search/SearchFormTest.yml
@@ -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ä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äcker
File:
- showInSearchFile:
- Title: showInSearchFile
- ShowInSearch: 1
- dontShowInSearchFile:
- Title: dontShowInSearchFile
- ShowInSearch: 0
+ showInSearchFile:
+ Title: showInSearchFile
+ ShowInSearch: 1
+ dontShowInSearchFile:
+ Title: dontShowInSearchFile
+ ShowInSearch: 0