API Update to use new Versioned API

API CMS notifications for recursive operations
API Un-deprecated delete action and batch actions
API remove deprecated API
This commit is contained in:
Damian Mooyman 2016-03-17 13:02:50 +13:00
parent 849cd898a4
commit 4cc7b0806d
17 changed files with 397 additions and 845 deletions

View File

@ -119,7 +119,6 @@ class CMSBatchAction_Restore extends CMSBatchAction {
*
* @package cms
* @subpackage batchaction
* @deprecated since version 4.0
*/
class CMSBatchAction_Delete extends CMSBatchAction {
public function getActionTitle() {
@ -127,7 +126,6 @@ class CMSBatchAction_Delete extends CMSBatchAction {
}
public function run(SS_List $pages) {
Deprecation::notice('4.0', 'Delete is deprecated. Use Archive instead');
$status = array(
'modified'=>array(),
'deleted'=>array(),
@ -163,53 +161,3 @@ class CMSBatchAction_Delete extends CMSBatchAction {
return $this->applicablePagesHelper($ids, 'canDelete', true, false);
}
}
/**
* Unpublish (delete from live site) items batch action.
*
* @package cms
* @subpackage batchaction
* @deprecated since version 4.0
*/
class CMSBatchAction_DeleteFromLive extends CMSBatchAction {
public function getActionTitle() {
return _t('CMSBatchActions.DELETE_PAGES', 'Delete from published site');
}
public function run(SS_List $pages) {
Deprecation::notice('4.0', 'Delete From Live is deprecated. Use Unpublish instead');
$status = array(
'modified'=>array(),
'deleted'=>array()
);
/** @var SiteTree $page */
foreach($pages as $page) {
$id = $page->ID;
// Perform the action
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(
'"SiteTree"."ID"' => $id
));
if($stageRecord) {
$status['modified'][$stageRecord->ID] = array(
'TreeTitle' => $stageRecord->TreeTitle,
);
} else {
$status['deleted'][$id] = array();
}
}
return $this->response(_t('CMSBatchActions.DELETED_PAGES', 'Deleted %d pages from published site, %d failures'), $status);
}
public function applicablePages($ids) {
return $this->applicablePagesHelper($ids, 'canDelete', false, true);
}
}

View File

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

View File

@ -58,14 +58,6 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
'childfilter',
);
/**
* Enable legacy batch actions.
* @deprecated since version 4.0
* @var array
* @config
*/
private static $enabled_legacy_actions = array();
public function init() {
// set reading lang
if(SiteTree::has_extension('Translatable') && !$this->getRequest()->isAjax()) {
@ -92,25 +84,9 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
CMSBatchActionHandler::register('publish', 'CMSBatchAction_Publish');
CMSBatchActionHandler::register('unpublish', 'CMSBatchAction_Unpublish');
// Check legacy actions
$legacy = $this->config()->enabled_legacy_actions;
// Delete from live is unnecessary since we have unpublish which does the same thing
if(in_array('CMSBatchAction_DeleteFromLive', $legacy)) {
Deprecation::notice('4.0', 'Delete From Live is deprecated. Use Un-publish instead');
CMSBatchActionHandler::register('deletefromlive', 'CMSBatchAction_DeleteFromLive');
}
// Delete action
if(in_array('CMSBatchAction_Delete', $legacy)) {
Deprecation::notice('4.0', 'Delete from Stage is deprecated. Use Archive instead.');
CMSBatchActionHandler::register('delete', 'CMSBatchAction_Delete');
} else {
CMSBatchActionHandler::register('archive', 'CMSBatchAction_Archive');
CMSBatchActionHandler::register('restore', 'CMSBatchAction_Restore');
}
CMSBatchActionHandler::register('delete', 'CMSBatchAction_Delete');
CMSBatchActionHandler::register('archive', 'CMSBatchAction_Archive');
CMSBatchActionHandler::register('restore', 'CMSBatchAction_Restore');
}
public function index($request) {
@ -555,7 +531,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// Then, try getting a record from the live site
if(!$record) {
// $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id");
Versioned::reading_stage('Live');
Versioned::set_stage(Versioned::LIVE);
singleton($treeClass)->flushCache();
$record = DataObject::get_by_id($treeClass, $id);
@ -872,6 +848,11 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
/**
* Save and Publish page handler
*
* @param array $data
* @param Form $form
* @return SS_HTTPResponse
* @throws SS_HTTPResponse_Exception
*/
public function save($data, $form) {
$className = $this->stat('tree_class');
@ -879,19 +860,35 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// Existing or new record?
$id = $data['ID'];
if(substr($id,0,3) != 'new') {
/** @var SiteTree $record */
$record = DataObject::get_by_id($className, $id);
if($record && !$record->canEdit()) return Security::permissionFailure($this);
if(!$record || !$record->ID) throw new SS_HTTPResponse_Exception("Bad record ID #$id", 404);
// Check edit permissions
if($record && !$record->canEdit()) {
return Security::permissionFailure($this);
}
if(!$record || !$record->ID) {
throw new SS_HTTPResponse_Exception("Bad record ID #$id", 404);
}
} else {
if(!singleton($this->stat('tree_class'))->canCreate()) return Security::permissionFailure($this);
if(!$className::singleton()->canCreate()) {
return Security::permissionFailure($this);
}
$record = $this->getNewItem($id, false);
}
// Check publishing permissions
$doPublish = !empty($data['publish']);
if($record && $doPublish && !$record->canPublish()) {
return Security::permissionFailure($this);
}
// TODO Coupling to SiteTree
$record->HasBrokenLink = 0;
$record->HasBrokenFile = 0;
if (!$record->ObsoleteClassName) $record->writeWithoutVersion();
if (!$record->ObsoleteClassName) {
$record->writeWithoutVersion();
}
// Update the class instance if necessary
if(isset($data['ClassName']) && $data['ClassName'] != $record->ClassName) {
@ -909,10 +906,22 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$record->write();
// If the 'Save & Publish' button was clicked, also publish the page
if (isset($data['publish']) && $data['publish'] == 1) {
if ($doPublish) {
$record->doPublish();
$message = _t(
'CMSMain.PUBLISHED',
"Published '{title}' successfully.",
['title' => $record->Title]
);
} else {
$message = _t(
'CMSMain.SAVED',
"Saved '{title}' successfully.",
['title' => $record->Title]
);
}
$this->getResponse()->addHeader('X-Status', rawurlencode($message));
return $this->getResponseNegotiator()->respond($this->getRequest());
}
@ -967,98 +976,56 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $newItem;
}
/**
* Delete the page from live. This means a page in draft mode might still exist.
*
* @see delete()
*/
public function deletefromlive($data, $form) {
Versioned::reading_stage('Live');
/** @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;
// before deleting the records, get the descendants of this tree
if($record) {
$descendantIDs = $record->getDescendantIDList();
// 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->doUnpublish();
$descendantsRemoved++;
}
// delete the record
$record->doUnpublish();
}
Versioned::reading_stage('Stage');
if(isset($descendantsRemoved)) {
$descRemoved = ' ' . _t(
'CMSMain.DESCREMOVED',
'and {count} descendants',
array('count' => $descendantsRemoved)
);
} else {
$descRemoved = '';
}
$this->getResponse()->addHeader(
'X-Status',
rawurlencode(
_t(
'CMSMain.REMOVED',
'Deleted \'{title}\'{description} from live site',
array('title' => $recordTitle, 'description' => $descRemoved)
)
)
);
// Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
return $this->getResponseNegotiator()->respond($this->getRequest());
}
/**
* Actually perform the publication step
*
* @param Versioned|DataObject $record
* @return mixed
*/
public function performPublish($record) {
if($record && !$record->canPublish()) return Security::permissionFailure($this);
if($record && !$record->canPublish()) {
return Security::permissionFailure($this);
}
$record->doPublish();
}
/**
* Reverts a page by publishing it to live.
* Use {@link restorepage()} if you want to restore a page
* which was deleted from draft without publishing.
*
* @uses SiteTree->doRevertToLive()
* Reverts a page by publishing it to live.
* Use {@link restorepage()} if you want to restore a page
* which was deleted from draft without publishing.
*
* @uses SiteTree->doRevertToLive()
*
* @param array $data
* @param Form $form
* @return SS_HTTPResponse
* @throws SS_HTTPResponse_Exception
*/
public function revert($data, $form) {
if(!isset($data['ID'])) return new SS_HTTPResponse("Please pass an ID in the form content", 400);
if(!isset($data['ID'])) {
throw new SS_HTTPResponse_Exception("Please pass an ID in the form content", 400);
}
$id = (int) $data['ID'];
$restoredPage = Versioned::get_latest_version("SiteTree", $id);
if(!$restoredPage) return new SS_HTTPResponse("SiteTree #$id not found", 400);
if(!$restoredPage) {
throw new SS_HTTPResponse_Exception("SiteTree #$id not found", 400);
}
/** @var SiteTree $record */
$record = Versioned::get_one_by_stage('SiteTree', 'Live', array(
'"SiteTree_Live"."ID"' => $id
));
// a user can restore a page without publication rights, as it just adds a new draft state
// (this action should just be available when page has been "deleted from draft")
if($record && !$record->canEdit()) return Security::permissionFailure($this);
if(!$record || !$record->ID) throw new SS_HTTPResponse_Exception("Bad record ID #$id", 404);
if($record && !$record->canEdit()) {
return Security::permissionFailure($this);
}
if(!$record || !$record->ID) {
throw new SS_HTTPResponse_Exception("Bad record ID #$id", 404);
}
$record->doRevertToLive();
@ -1077,14 +1044,23 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
/**
* Delete the current page from draft stage.
*
* @see deletefromlive()
*
* @param array $data
* @param Form $form
* @return SS_HTTPResponse
* @throws SS_HTTPResponse_Exception
*/
public function delete($data, $form) {
Deprecation::notice('4.0', 'Delete from stage is deprecated. Use archive instead');
$id = $data['ID'];
$record = DataObject::get_by_id("SiteTree", $id);
if($record && !$record->canDelete()) return Security::permissionFailure();
if(!$record || !$record->ID) throw new SS_HTTPResponse_Exception("Bad record ID #$id", 404);
if($record && !$record->canDelete()) {
return Security::permissionFailure();
}
if(!$record || !$record->ID) {
throw new SS_HTTPResponse_Exception("Bad record ID #$id", 404);
}
// Delete record
$record->delete();
@ -1103,9 +1079,12 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
*
* @param array $data
* @param Form $form
* @return SS_HTTPResponse
* @throws SS_HTTPResponse_Exception
*/
public function archive($data, $form) {
$id = $data['ID'];
/** @var SiteTree $record */
$record = DataObject::get_by_id("SiteTree", $id);
if(!$record || !$record->exists()) {
throw new SS_HTTPResponse_Exception("Bad record ID #$id", 404);
@ -1167,10 +1146,9 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
/**
* Rolls a site back to a given version ID
*
* @param array
* @param Form
*
* @return html
* @param array $data
* @param Form $form
* @return SS_HTTPResponse
*/
public function doRollback($data, $form) {
$this->extend('onBeforeRollback', $data['ID']);
@ -1178,8 +1156,11 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$id = (isset($data['ID'])) ? (int) $data['ID'] : null;
$version = (isset($data['Version'])) ? (int) $data['Version'] : null;
/** @var DataObject|Versioned $record */
$record = DataObject::get_by_id($this->stat('tree_class'), $id);
if($record && !$record->canEdit()) return Security::permissionFailure($this);
if($record && !$record->canEdit()) {
return Security::permissionFailure($this);
}
if($version) {
$record->doRollbackTo($version);

View File

@ -297,7 +297,7 @@ class SilverStripeNavigatorItem_StageLink extends SilverStripeNavigatorItem {
public function isActive() {
return (
Versioned::current_stage() == 'Stage'
Versioned::get_stage() == 'Stage'
&& !(ClassInfo::exists('SiteTreeFutureState') && SiteTreeFutureState::get_future_datetime())
&& !$this->isArchived()
);
@ -350,7 +350,7 @@ class SilverStripeNavigatorItem_LiveLink extends SilverStripeNavigatorItem {
public function isActive() {
return (
(!Versioned::current_stage() || Versioned::current_stage() == 'Live')
(!Versioned::get_stage() || Versioned::get_stage() == 'Live')
&& !$this->isArchived()
);
}

View File

@ -132,6 +132,14 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
"EditorGroups" => "Group",
);
private static $has_many = array(
"VirtualPages" => "VirtualPage.CopyContentFrom"
);
private static $owned_by = array(
"VirtualPages"
);
private static $casting = array(
"Breadcrumbs" => "HTMLText",
"LastEdited" => "SS_Datetime",
@ -181,7 +189,7 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
private static $extensions = array(
"Hierarchy",
"Versioned('Stage', 'Live')",
"Versioned",
"SiteTreeLinkTracking"
);
@ -233,77 +241,6 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
protected $_cache_statusFlags = null;
/**
* Determines if the system should avoid orphaned pages
* by deleting all children when the their parent is deleted (TRUE),
* or rather preserve this data even if its not reachable through any navigation path (FALSE).
*
* @deprecated 4.0 Use the "SiteTree.enforce_strict_hierarchy" config setting instead
* @param boolean
*/
static public function set_enforce_strict_hierarchy($to) {
Deprecation::notice('4.0', 'Use the "SiteTree.enforce_strict_hierarchy" config setting instead');
Config::inst()->update('SiteTree', 'enforce_strict_hierarchy', $to);
}
/**
* @deprecated 4.0 Use the "SiteTree.enforce_strict_hierarchy" config setting instead
* @return boolean
*/
static public function get_enforce_strict_hierarchy() {
Deprecation::notice('4.0', 'Use the "SiteTree.enforce_strict_hierarchy" config setting instead');
return Config::inst()->get('SiteTree', 'enforce_strict_hierarchy');
}
/**
* Returns TRUE if nested URLs (e.g. page/sub-page/) are currently enabled on this site.
*
* @deprecated 4.0 Use the "SiteTree.nested_urls" config setting instead
* @return bool
*/
static public function nested_urls() {
Deprecation::notice('4.0', 'Use the "SiteTree.nested_urls" config setting instead');
return Config::inst()->get('SiteTree', 'nested_urls');
}
/**
* @deprecated 4.0 Use the "SiteTree.nested_urls" config setting instead
*/
static public function enable_nested_urls() {
Deprecation::notice('4.0', 'Use the "SiteTree.nested_urls" config setting instead');
Config::inst()->update('SiteTree', 'nested_urls', true);
}
/**
* @deprecated 4.0 Use the "SiteTree.nested_urls" config setting instead
*/
static public function disable_nested_urls() {
Deprecation::notice('4.0', 'Use the "SiteTree.nested_urls" config setting instead');
Config::inst()->update('SiteTree', 'nested_urls', false);
}
/**
* Set the (re)creation of default pages on /dev/build
*
* @deprecated 4.0 Use the "SiteTree.create_default_pages" config setting instead
* @param bool $option
*/
static public function set_create_default_pages($option = true) {
Deprecation::notice('4.0', 'Use the "SiteTree.create_default_pages" config setting instead');
Config::inst()->update('SiteTree', 'create_default_pages', $option);
}
/**
* Return true if default pages should be created on /dev/build.
*
* @deprecated 4.0 Use the "SiteTree.create_default_pages" config setting instead
* @return bool
*/
static public function get_create_default_pages() {
Deprecation::notice('4.0', 'Use the "SiteTree.create_default_pages" config setting instead');
return Config::inst()->get('SiteTree', 'create_default_pages');
}
/**
* Fetches the {@link SiteTree} object that maps to a link.
*
@ -439,7 +376,7 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
!($page = DataObject::get_by_id('SiteTree', $arguments['id'])) // Get the current page by ID.
&& !($page = Versioned::get_latest_version('SiteTree', $arguments['id'])) // Attempt link to old version.
) {
return; // There were no suitable matches at all.
return null; // There were no suitable matches at all.
}
$link = Convert::raw2att($page->Link());
@ -538,9 +475,9 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
* @return string
*/
public function getAbsoluteLiveLink($includeStageEqualsLive = true) {
$oldStage = Versioned::current_stage();
Versioned::reading_stage('Live');
$live = Versioned::get_one_by_stage('SiteTree', 'Live', array(
$oldStage = Versioned::get_stage();
Versioned::set_stage(Versioned::LIVE);
$live = Versioned::get_one_by_stage('SiteTree', Versioned::LIVE, array(
'"SiteTree"."ID"' => $this->ID
));
if($live) {
@ -550,7 +487,7 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
$link = null;
}
Versioned::reading_stage($oldStage);
Versioned::set_stage($oldStage);
return $link;
}
@ -1067,22 +1004,6 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
}
}
/**
* @deprecated
*/
public function canDeleteFromLive($member = null) {
Deprecation::notice('4.0', 'Use canUnpublish');
// 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.
*
@ -1531,26 +1452,6 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
$this->extend('augmentSyncLinkTracking');
}
public function onAfterWrite() {
// Need to flush cache to avoid outdated versionnumber references
$this->flushCache();
$linkedPages = $this->VirtualPages();
if($linkedPages) {
// The only way after a write() call to determine if it was triggered by a writeWithoutVersion(),
// which we have to pass on to the virtual page writes as well.
$previous = ($this->Version > 1) ? Versioned::get_version($this->class, $this->ID, $this->Version-1) : null;
$withoutVersion = $this->getExtensionInstance('Versioned')->_nextWriteWithoutVersion;
foreach($linkedPages as $page) {
$page->copyFrom($page->CopyContentFrom());
if($withoutVersion) $page->writeWithoutVersion();
else $page->write();
}
}
parent::onAfterWrite();
}
public function onBeforeDelete() {
parent::onBeforeDelete();
@ -1693,7 +1594,7 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
* @return string
*/
public function getStageURLSegment() {
$stageRecord = Versioned::get_one_by_stage('SiteTree', 'Stage', array(
$stageRecord = Versioned::get_one_by_stage('SiteTree', Versioned::DRAFT, array(
'"SiteTree"."ID"' => $this->ID
));
return ($stageRecord) ? $stageRecord->URLSegment : null;
@ -1705,7 +1606,7 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
* @return string
*/
public function getLiveURLSegment() {
$liveRecord = Versioned::get_one_by_stage('SiteTree', 'Live', array(
$liveRecord = Versioned::get_one_by_stage('SiteTree', Versioned::LIVE, array(
'"SiteTree"."ID"' => $this->ID
));
return ($liveRecord) ? $liveRecord->URLSegment : null;
@ -1720,7 +1621,7 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
*/
public function rewriteFileLinks() {
// Skip live stage
if(\Versioned::current_stage() === \Versioned::get_live_stage()) {
if(\Versioned::get_stage() === \Versioned::LIVE) {
return;
}
@ -1814,26 +1715,14 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
* @return DataList
*/
public function VirtualPages() {
$pages = parent::VirtualPages();
// Ignore new records
if(!$this->ID) return null;
// Check subsite virtual pages
// @todo Refactor out subsite module specific code
if(class_exists('Subsite')) {
return Subsite::get_from_all_subsites('VirtualPage', array(
'"VirtualPage"."CopyContentFromID"' => $this->ID
));
// Disable subsite filter for these pages
if($pages instanceof DataList) {
return $pages->setDataQueryParam('Subsite.filter', false);
} else {
return $pages;
}
// Check existing virtualpages
if(class_exists('VirtualPage')) {
return VirtualPage::get()->where(array(
'"VirtualPage"."CopyContentFromID"' => $this->ID
));
}
return null;
}
/**
@ -2213,7 +2102,7 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
$rootTabSet->addExtraClass('ss-ui-action-tabset action-menus noborder');
// Render page information into the "more-options" drop-up, on the top.
$live = Versioned::get_one_by_stage('SiteTree', 'Live', array(
$live = Versioned::get_one_by_stage('SiteTree', Versioned::LIVE, array(
'"SiteTree"."ID"' => $this->ID
));
$moreOptions->push(
@ -2226,7 +2115,7 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
);
// "readonly"/viewing version that isn't the current version of the record
$stageOrLiveRecord = Versioned::get_one_by_stage($this->class, Versioned::current_stage(), array(
$stageOrLiveRecord = Versioned::get_one_by_stage($this->class, Versioned::get_stage(), array(
'"SiteTree"."ID"' => $this->ID
));
if($stageOrLiveRecord && $stageOrLiveRecord->Version != $this->Version) {
@ -2292,19 +2181,14 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
);
}
} else {
// Detect use of legacy actions
// {@see CMSMain::enabled_legacy_actions}
$legacy = CMSMain::config()->enabled_legacy_actions;
if(in_array('CMSBatchAction_Delete', $legacy)) {
Deprecation::notice('4.0', 'Delete from Stage is deprecated. Use Archive instead.');
if($this->canDelete()) {
// delete
$moreOptions->push(
FormAction::create('delete',_t('CMSMain.DELETE','Delete draft'))
->addExtraClass('delete ss-ui-action-destructive')
);
}
} elseif($this->canArchive()) {
if($this->canDelete()) {
// delete
$moreOptions->push(
FormAction::create('delete',_t('CMSMain.DELETE','Delete draft'))
->addExtraClass('delete ss-ui-action-destructive')
);
}
if($this->canArchive()) {
// "archive"
$moreOptions->push(
FormAction::create('archive',_t('CMSMain.ARCHIVE','Archive'))
@ -2349,123 +2233,29 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
return $actions;
}
/**
* Publish this page.
*
* @uses SiteTreeExtension->onBeforePublish()
* @uses SiteTreeExtension->onAfterPublish()
* @return bool True if published
*/
public function doPublish() {
if (!$this->canPublish()) return false;
$original = Versioned::get_one_by_stage("SiteTree", "Live", array(
'"SiteTree"."ID"' => $this->ID
));
if(!$original) $original = new SiteTree();
// Handle activities undertaken by extensions
$this->invokeWithExtensions('onBeforePublish', $original);
//$this->PublishedByID = Member::currentUser()->ID;
$this->write();
$this->publish("Stage", "Live");
public function onAfterPublish() {
// Force live sort order to match stage sort order
DB::prepared_query('UPDATE "SiteTree_Live"
SET "Sort" = (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID")
WHERE EXISTS (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID") AND "ParentID" = ?',
array($this->ParentID)
);
// Publish any virtual pages that might need publishing
$linkedPages = $this->VirtualPages();
if($linkedPages) foreach($linkedPages as $page) {
$page->copyFrom($page->CopyContentFrom());
$page->write();
if($page->getExistsOnLive()) $page->doPublish();
}
// Need to update pages linking to this one as no longer broken, on the live site
$origMode = Versioned::get_reading_mode();
Versioned::reading_stage('Live');
foreach($this->DependentPages(false) as $page) {
// $page->write() calls syncLinkTracking, which does all the hard work for us.
$page->write();
}
Versioned::set_reading_mode($origMode);
// Handle activities undertaken by extensions
$this->invokeWithExtensions('onAfterPublish', $original);
return true;
}
/**
* Unpublish this page - remove it from the live site
*
* Overrides {@see Versioned::doUnpublish()}
*
* @uses SiteTreeExtension->onBeforeUnpublish()
* @uses SiteTreeExtension->onAfterUnpublish()
* Update draft dependant pages
*/
public function doUnpublish() {
if(!$this->canUnpublish()) return false;
if(!$this->ID) return false;
$this->invokeWithExtensions('onBeforeUnpublish', $this);
$origStage = Versioned::current_stage();
Versioned::reading_stage('Live');
// We should only unpublish virtualpages that exist on live
$virtualPages = $this->VirtualPages();
// This way our ID won't be unset
$clone = clone $this;
$clone->delete();
// Rewrite backlinks
$dependentPages = $this->DependentPages(false);
if($dependentPages) foreach($dependentPages as $page) {
// $page->write() calls syncLinkTracking, which does all the hard work for us.
$page->write();
}
Versioned::reading_stage($origStage);
// Unpublish any published virtual pages
if ($virtualPages) foreach($virtualPages as $vp) $vp->doUnpublish();
// If we're on the draft site, then we can update the status.
// Otherwise, these lines will resurrect an inappropriate record
if(DB::prepared_query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($this->ID))->value()
&& Versioned::current_stage() != 'Live') {
$this->write();
}
$this->invokeWithExtensions('onAfterUnpublish', $this);
return true;
}
/**
* Revert the draft changes: replace the draft content with the content on live
*/
public function doRevertToLive() {
$this->invokeWithExtensions('onBeforeRevertToLive', $this);
$this->publish("Live", "Stage", false);
// Use a clone to get the updates made by $this->publish
$clone = DataObject::get_by_id("SiteTree", $this->ID);
$clone->writeWithoutVersion();
public function onAfterRevertToLive() {
// Use an alias to get the updates made by $this->publish
/** @var SiteTree $stageSelf */
$stageSelf = Versioned::get_by_stage('SiteTree', Versioned::DRAFT)->byID($this->ID);
$stageSelf->writeWithoutVersion();
// Need to update pages linking to this one as no longer broken
foreach($this->DependentPages(false) as $page) {
// $page->write() calls syncLinkTracking, which does all the hard work for us.
$page->write();
foreach($stageSelf->DependentPages() as $page) {
/** @var SiteTree $page */
$page->writeWithoutVersion();
}
$this->invokeWithExtensions('onAfterRevertToLive', $this);
return true;
}
/**
@ -2505,8 +2295,8 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing('SiteTree', false);
}
$oldStage = Versioned::current_stage();
Versioned::reading_stage('Stage');
$oldStage = Versioned::get_stage();
Versioned::set_stage(Versioned::DRAFT);
$this->forceChange();
$this->write();
@ -2518,21 +2308,13 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
$page->write();
}
Versioned::reading_stage($oldStage);
Versioned::set_stage($oldStage);
$this->invokeWithExtensions('onAfterRestoreToStage', $this);
return $result;
}
/**
* @deprecated
*/
public function doDeleteFromLive() {
Deprecation::notice("4.0", "Use doUnpublish instead");
return $this->doUnpublish();
}
/**
* Check if this page is new - that is, if it has yet to have been written to the database.
*

View File

@ -109,7 +109,7 @@ class SiteTreeFileExtension extends DataExtension {
*/
public function onAfterDelete() {
// Skip live stage
if(\Versioned::current_stage() === \Versioned::get_live_stage()) {
if(\Versioned::get_stage() === Versioned::LIVE) {
return;
}
@ -143,7 +143,7 @@ class SiteTreeFileExtension extends DataExtension {
*/
public function updateLinks() {
// Skip live stage
if(\Versioned::current_stage() === \Versioned::get_live_stage()) {
if(\Versioned::get_stage() === \Versioned::LIVE) {
return;
}

View File

@ -28,12 +28,39 @@
*/
class SiteTreeLinkTracking extends DataExtension {
public $parser;
/**
* @var SiteTreeLinkTracking_Parser
*/
protected $parser;
/**
* Inject parser for each page
*
* @var array
* @config
*/
private static $dependencies = array(
'parser' => '%$SiteTreeLinkTracking_Parser'
'Parser' => '%$SiteTreeLinkTracking_Parser'
);
/**
* Parser for link tracking
*
* @return SiteTreeLinkTracking_Parser
*/
public function getParser() {
return $this->parser;
}
/**
* @param SiteTreeLinkTracking_Parser $parser
* @return $this
*/
public function setParser($parser) {
$this->parser = $parser;
return $this;
}
private static $db = array(
"HasBrokenFile" => "Boolean",
"HasBrokenLink" => "Boolean"
@ -178,7 +205,7 @@ class SiteTreeLinkTracking extends DataExtension {
*/
public function augmentSyncLinkTracking() {
// Skip live tracking
if(\Versioned::current_stage() == \Versioned::get_live_stage()) {
if(\Versioned::get_stage() == \Versioned::LIVE) {
return;
}

View File

@ -3,6 +3,9 @@
* Virtual Page creates an instance of a page, with the same fields that the original page had, but readonly.
* This allows you can have a page in mulitple places in the site structure, with different children without duplicating the content
* Note: This Only duplicates $db fields and not the $has_one etc..
*
* @method SiteTree CopyContentFrom()
*
* @package cms
*/
class VirtualPage extends Page {
@ -18,8 +21,10 @@ class VirtualPage extends Page {
private static $non_virtual_fields = array(
"ID",
"ClassName",
"ObsoleteClassName",
"SecurityTypeID",
"OwnerID",
"ParentID",
"URLSegment",
"Sort",
"Status",
@ -31,6 +36,8 @@ class VirtualPage extends Page {
"Expiry",
"CanViewType",
"CanEditType",
"CopyContentFromID",
"HasBrokenLink",
);
/**
@ -46,57 +53,43 @@ class VirtualPage extends Page {
"CopyContentFrom" => "SiteTree",
);
private static $owns = array(
"CopyContentFrom",
);
private static $db = array(
"VersionID" => "Int",
);
/**
* Generates the array of fields required for the page type.
*
* @return array
*/
public function getVirtualFields() {
$nonVirtualFields = array_merge(self::config()->non_virtual_fields, self::config()->initially_copied_fields);
// Check if copied page exists
$record = $this->CopyContentFrom();
if(!$record || !$record->exists()) {
return array();
}
$virtualFields = array();
foreach($record->db() as $field => $type) {
if(!in_array($field, $nonVirtualFields)) {
$virtualFields[] = $field;
}
}
return $virtualFields;
// Diff db with non-virtual fields
$fields = array_keys($record->db());
$nonVirtualFields = $this->getNonVirtualisedFields();
return array_diff($fields, $nonVirtualFields);
}
/**
* Returns the linked page, or failing that, a new object.
* List of fields or properties to never virtualise
*
* Always returns a non-empty object
*
* @return SiteTree
* @return array
*/
public function CopyContentFrom() {
$copyContentFromID = $this->CopyContentFromID;
if(!$copyContentFromID) {
return new SiteTree();
}
if(!isset($this->components['CopyContentFrom'])) {
$this->components['CopyContentFrom'] = DataObject::get_by_id("SiteTree", $copyContentFromID);
// Don't let VirtualPages point to other VirtualPages
if($this->components['CopyContentFrom'] instanceof VirtualPage) {
$this->components['CopyContentFrom'] = null;
}
// has_one component semantics incidate than an empty object should be returned
if(!$this->components['CopyContentFrom']) {
$this->components['CopyContentFrom'] = new SiteTree();
}
}
return $this->components['CopyContentFrom'] ? $this->components['CopyContentFrom'] : new SiteTree();
public function getNonVirtualisedFields() {
return array_merge($this->config()->non_virtual_fields, $this->config()->initially_copied_fields);
}
public function setCopyContentFromID($val) {
// Sanity check to prevent pages virtualising other virtual pages
if($val && DataObject::get_by_id('SiteTree', $val) instanceof VirtualPage) {
$val = 0;
}
@ -104,7 +97,11 @@ class VirtualPage extends Page {
}
public function ContentSource() {
return $this->CopyContentFrom();
$copied = $this->CopyContentFrom();
if($copied && $copied->exists()) {
return $copied;
}
return $this;
}
/**
@ -116,15 +113,18 @@ class VirtualPage extends Page {
*/
public function MetaTags($includeTitle = true) {
$tags = parent::MetaTags($includeTitle);
if ($this->CopyContentFrom()->ID) {
$tags .= "<link rel=\"canonical\" href=\"{$this->CopyContentFrom()->Link()}\" />\n";
$copied = $this->CopyContentFrom();
if ($copied && $copied->exists()) {
$link = Convert::raw2att($copied->Link());
$tags .= "<link rel=\"canonical\" href=\"{$link}\" />\n";
}
return $tags;
}
public function allowedChildren() {
if($this->CopyContentFrom()) {
return $this->CopyContentFrom()->allowedChildren();
$copy = $this->CopyContentFrom();
if($copy && $copy->exists()) {
return $copy->allowedChildren();
}
return array();
}
@ -237,6 +237,35 @@ class VirtualPage extends Page {
return $fields;
}
public function onBeforeWrite() {
parent::onBeforeWrite();
$this->refreshFromCopied();
}
/**
* Copy any fields from the copied record to bootstrap /backup
*/
protected function refreshFromCopied() {
// Skip if copied record isn't available
$source = $this->CopyContentFrom();
if(!$source || !$source->exists()) {
return;
}
// 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) {
$this->$fieldName = $source->$fieldName;
}
}
// Copy fields to the original record in case the class type changes
foreach($this->getVirtualFields() as $virtualField) {
$this->$virtualField = $source->$virtualField;
}
}
public function getSettingsFields() {
$fields = parent::getSettingsFields();
if(!$this->CopyContentFrom()->exists()) {
@ -257,106 +286,12 @@ class VirtualPage extends Page {
return $fields;
}
/**
* We have to change it to copy all the content from the original page first.
*/
public function onBeforeWrite() {
$performCopyFrom = null;
// Determine if we need to copy values.
if(
$this->extension_instances['Versioned']->migratingVersion
&& Versioned::current_stage() == 'Live'
&& $this->CopyContentFromID
) {
// On publication to live, copy from published source.
$performCopyFrom = true;
$stageSourceVersion = DB::prepared_query(
'SELECT "Version" FROM "SiteTree" WHERE "ID" = ?',
array($this->CopyContentFromID)
)->value();
$liveSourceVersion = DB::prepared_query(
'SELECT "Version" FROM "SiteTree_Live" WHERE "ID" = ?',
array($this->CopyContentFromID)
)->value();
// We're going to create a new VP record in SiteTree_versions because the published
// version might not exist, unless we're publishing the latest version
if($stageSourceVersion != $liveSourceVersion) {
$this->extension_instances['Versioned']->migratingVersion = null;
}
} else {
// On regular write, copy from draft source. This is only executed when the source page changes.
$performCopyFrom = $this->isChanged('CopyContentFromID', 2) && $this->CopyContentFromID != 0;
}
if($performCopyFrom && $this instanceof VirtualPage) {
// This flush is needed because the get_one cache doesn't respect site version :-(
singleton('SiteTree')->flushCache();
// @todo Update get_one to support parameterised queries
$source = DataObject::get_by_id("SiteTree", $this->CopyContentFromID);
// Leave the updating of image tracking until after write, in case its a new record
$this->copyFrom($source, false);
}
parent::onBeforeWrite();
}
public function onAfterWrite() {
parent::onAfterWrite();
// Don't do this stuff when we're publishing
if(!$this->extension_instances['Versioned']->migratingVersion) {
if(
$this->isChanged('CopyContentFromID')
&& $this->CopyContentFromID != 0
&& $this instanceof VirtualPage
) {
$this->updateImageTracking();
}
}
// Check if page type has changed to a non-virtual page.
// Caution: Relies on the fact that the current instance is still of the old page type.
if($this->isChanged('ClassName', 2)) {
$changed = $this->getChangedFields();
$classBefore = $changed['ClassName']['before'];
$classAfter = $changed['ClassName']['after'];
if($classBefore != $classAfter) {
// Remove all database rows for the old page type to avoid inconsistent data retrieval.
// TODO This should apply to all page type changes, not only on VirtualPage - but needs
// more comprehensive testing as its a destructive operation
$removedTables = array_diff(ClassInfo::dataClassesFor($classBefore), ClassInfo::dataClassesFor($classAfter));
if($removedTables) foreach($removedTables as $removedTable) {
// Note: *_versions records are left intact
foreach(array('', 'Live') as $stage) {
if($stage) $removedTable = "{$removedTable}_{$stage}";
DB::prepared_query("DELETE FROM \"$removedTable\" WHERE \"ID\" = ?", array($this->ID));
}
}
// Also publish the change immediately to avoid inconsistent behaviour between
// a non-virtual draft and a virtual live record (e.g. republishing the original record
// shouldn't republish the - now unrelated - changes on the ex-VirtualPage draft).
// Copies all stage fields to live as well.
// @todo Update get_one to support parameterised queries
$source = DataObject::get_by_id("SiteTree", $this->CopyContentFromID);
$this->copyFrom($source);
$this->publish('Stage', 'Live');
// Change reference on instance (as well as removing the underlying database tables)
$this->CopyContentFromID = 0;
}
}
}
public function validate() {
$result = parent::validate();
// "Can be root" validation
$orig = $this->CopyContentFrom();
if(!$orig->stat('can_be_root') && !$this->ParentID) {
if($orig && $orig->exists() && !$orig->stat('can_be_root') && !$this->ParentID) {
$result->error(
_t(
'VirtualPage.PageTypNotAllowedOnRoot',
@ -370,30 +305,6 @@ class VirtualPage extends Page {
return $result;
}
/**
* Ensure we have an up-to-date version of everything.
*
* @param DataObject $source
* @param bool $updateImageTracking
*/
public function copyFrom($source, $updateImageTracking = true) {
if($source) {
foreach($this->getVirtualFields() as $virtualField) {
$this->$virtualField = $source->$virtualField;
}
// 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 != 0) {
foreach(self::config()->initially_copied_fields as $fieldName) {
$this->$fieldName = $source->$fieldName;
}
}
if($updateImageTracking) $this->updateImageTracking();
}
}
public function updateImageTracking() {
// Doesn't work on unsaved records
if(!$this->ID) return;
@ -425,11 +336,41 @@ class VirtualPage extends Page {
return $this->$funcName();
} else if(parent::hasField($field) || ($field === 'ID' && !$this->exists())) {
return $this->getField($field);
} else {
return $this->copyContentFrom()->$field;
} elseif(($copy = $this->CopyContentFrom()) && $copy->exists()) {
return $copy->$field;
}
}
public function getField($field) {
if($this->isFieldVirtualised($field)) {
return $this->CopyContentFrom()->getField($field);
}
return parent::getField($field);
}
/**
* Check if given field is virtualised
*
* @param string $field
* @return bool
*/
public function isFieldVirtualised($field) {
// Don't defer if field is non-virtualised
$ignore = $this->getNonVirtualisedFields();
if(in_array($field, $ignore)) {
return false;
}
// Don't defer if no virtual page
$copied = $this->CopyContentFrom();
if(!$copied || !$copied->exists()) {
return false;
}
// Check if copied object has this field
return $copied->hasField($field);
}
/**
* Pass unrecognized method calls on to the original data object
*
@ -452,9 +393,11 @@ class VirtualPage extends Page {
public function hasField($field) {
if(parent::hasField($field)) {
return true;
}
$copy = $this->CopyContentFrom();
return $copy && $copy->exists() && $copy->hasField($field);
}
return $this->CopyContentFrom()->hasField($field);
}
/**
* Overwrite to also check for method on the original data object
*
@ -464,8 +407,13 @@ class VirtualPage extends Page {
public function hasMethod($method) {
if(parent::hasMethod($method)) {
return true;
}
return $this->CopyContentFrom()->hasMethod($method);
}
// Don't call property setters on copied page
if(stripos($method, 'set') === 0) {
return false;
}
$copy = $this->CopyContentFrom();
return $copy && $copy->exists() && $copy->hasMethod($method);
}
/**
@ -476,9 +424,11 @@ class VirtualPage extends Page {
* @return string
*/
public function castingHelper($field) {
return $this
->CopyContentFrom()
->castingHelper($field);
$copy = $this->CopyContentFrom();
if($copy && $copy->exists() && ($helper = $copy->castingHelper($field))) {
return $helper;
}
return parent::castingHelper($field);
}
}
@ -493,15 +443,6 @@ class VirtualPage_Controller extends Page_Controller {
'loadcontentall' => 'ADMIN',
);
/**
* Reloads the content if the version is different ;-)
*/
public function reloadContent() {
$this->failover->copyFrom($this->failover->CopyContentFrom());
$this->failover->write();
return;
}
public function getViewer($action) {
$originalClass = get_class($this->CopyContentFrom());
if ($originalClass == 'SiteTree') $name = 'Page_Controller';
@ -517,26 +458,10 @@ class VirtualPage_Controller extends Page_Controller {
* We can't load the content without an ID or record to copy it from.
*/
public function init(){
if(isset($this->record) && $this->record->ID){
if($this->record->VersionID != $this->failover->CopyContentFrom()->Version){
$this->reloadContent();
$this->VersionID = $this->failover->CopyContentFrom()->VersionID;
}
}
parent::init();
$this->__call('init', array());
}
public function loadcontentall() {
$pages = DataObject::get("VirtualPage");
foreach($pages as $page) {
$page->copyFrom($page->CopyContentFrom());
$page->write();
$page->publish("Stage", "Live");
echo "<li>Published $page->URLSegment";
}
}
/**
* Also check the original object's original controller for the method
*

View File

@ -10,22 +10,22 @@ class AssetAdminTest extends SapphireTest {
public function setUp() {
parent::setUp();
AssetStoreTest_SpyStore::activate('AssetAdminTest');
if(!file_exists(ASSETS_PATH)) mkdir(ASSETS_PATH);
// Create a test folders for each of the fixture references
$folderIDs = $this->allFixtureIDs('Folder');
foreach($folderIDs as $folderID) {
$folder = DataObject::get_by_id('Folder', $folderID);
if(!file_exists(BASE_PATH."/$folder->Filename")) mkdir(BASE_PATH."/$folder->Filename");
foreach(File::get()->filter('ClassName', 'Folder') as $folder) {
/** @var Folder $folder */
$folder->doPublish();
}
// Create a test files for each of the fixture references
$fileIDs = $this->allFixtureIDs('File');
foreach($fileIDs as $fileID) {
$file = DataObject::get_by_id('File', $fileID);
$fh = fopen(BASE_PATH."/$file->Filename", "w");
fwrite($fh, str_repeat('x',1000000));
fclose($fh);
$content = str_repeat('x',1000000);
foreach(File::get()->exclude('ClassName', 'Folder') as $file) {
/** @var File $file */
$file->setFromString($content, $file->generateFilename());
$file->doPublish();
}
}

View File

@ -4,9 +4,9 @@ Folder:
File:
file1:
Title: File1
Filename: assets/AssetAdminTest/file1.txt
Name: file1.txt
ParentID: =>Folder.folder1
file2:
Title: File2
Filename: assets/AssetAdminTest/file2.txt
Name: file2.txt
ParentID: =>Folder.folder1

View File

@ -11,6 +11,8 @@ class CMSBatchActionsTest extends SapphireTest {
public function setUp() {
parent::setUp();
$this->logInWithPermission('ADMIN');
// published page
$published = $this->objFromFixture('SiteTree', 'published');
$published->doPublish();

View File

@ -9,7 +9,7 @@ class ErrorPageFileExtensionTest extends SapphireTest {
public function setUp() {
parent::setUp();
$this->versionedMode = Versioned::get_reading_mode();
Versioned::reading_stage('Stage');
Versioned::set_stage(Versioned::DRAFT);
AssetStoreTest_SpyStore::activate('ErrorPageFileExtensionTest');
$file = new File();
$file->setFromString('dummy', 'dummy.txt');

View File

@ -9,7 +9,7 @@ class FileLinkTrackingTest extends SapphireTest {
public function setUp() {
parent::setUp();
Versioned::reading_stage('Stage');
Versioned::set_stage(Versioned::DRAFT);
AssetStoreTest_SpyStore::activate('FileLinkTrackingTest');
$this->logInWithPermission('ADMIN');
@ -112,18 +112,20 @@ class FileLinkTrackingTest extends SapphireTest {
$file->write();
// Verify that the draft virtual pages have the correct content
$svp = Versioned::get_by_stage('VirtualPage', Versioned::DRAFT)->byID($svp->ID);
$this->assertContains(
'<img src="/assets/55b443b601/renamed-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($svp->ID))->value()
$svp->Content
);
// Publishing both file and page will update the live record
$file->doPublish();
$page->doPublish();
$svp = Versioned::get_by_stage('VirtualPage', Versioned::LIVE)->byID($svp->ID);
$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()
$svp->Content
);
}

View File

@ -108,7 +108,7 @@ class SiteTreeBacklinksTest extends SapphireTest {
$page3live = Versioned::get_one_by_stage('Page', 'Live', '"SiteTree"."ID" = ' . $page3->ID);
// assert hyperlink to page 1's new url exists
Versioned::reading_stage('Live');
Versioned::set_stage(Versioned::LIVE);
$links = HTTP::getLinksIn($page3live->obj('Content')->forTemplate());
$this->assertContains(Director::baseURL().'new-url-segment/', $links, 'Assert hyperlink to page 1\'s new url exists on page 3');
}
@ -136,7 +136,7 @@ class SiteTreeBacklinksTest extends SapphireTest {
// assert hyperlink to page 1's current publish url exists
$page3live = Versioned::get_one_by_stage('Page', 'Live', '"SiteTree"."ID" = ' . $page3->ID);
Versioned::reading_stage('Live');
Versioned::set_stage(Versioned::LIVE);
$links = HTTP::getLinksIn($page3live->obj('Content')->forTemplate());
$this->assertContains(Director::baseURL().'page1/', $links, 'Assert hyperlink to page 1\'s current published url exists on page 3');
@ -179,7 +179,7 @@ class SiteTreeBacklinksTest extends SapphireTest {
// assert page 3 on published site contains old page 1 url
$page3live = Versioned::get_one_by_stage('Page', 'Live', '"SiteTree"."ID" = ' . $page3->ID);
Versioned::reading_stage('Live');
Versioned::set_stage(Versioned::LIVE);
$links = HTTP::getLinksIn($page3live->obj('Content')->forTemplate());
$this->assertContains(Director::baseURL().'page1/', $links, 'Assert hyperlink to page 1\'s current published url exists on page 3');
@ -223,7 +223,7 @@ class SiteTreeBacklinksTest extends SapphireTest {
// confirm that published link hasn't
$page2Live = Versioned::get_one_by_stage("Page", "Live", "\"SiteTree\".\"ID\" = $page2->ID");
Versioned::reading_stage('Live');
Versioned::set_stage(Versioned::LIVE);
$this->assertEquals('<p><a href="'.Director::baseURL().'page1/">Testing page 1 link</a></p>', $page2Live->obj('ExtraContent')->forTemplate());
// publish page1 and confirm that the link on the published page2 has now been updated

View File

@ -11,7 +11,7 @@ class SiteTreeBrokenLinksTest extends SapphireTest {
public function setUp() {
parent::setUp();
Versioned::reading_stage('Stage');
Versioned::set_stage(Versioned::DRAFT);
AssetStoreTest_SpyStore::activate('SiteTreeBrokenLinksTest');
$this->logInWithPermission('ADMIN');
}

View File

@ -141,7 +141,7 @@ class SiteTreeTest extends SapphireTest {
$s->write();
$oldMode = Versioned::get_reading_mode();
Versioned::reading_stage('Live');
Versioned::set_stage(Versioned::LIVE);
$checkSiteTree = DataObject::get_one("SiteTree", array(
'"SiteTree"."URLSegment"' => 'get-one-test-page'
@ -264,12 +264,12 @@ class SiteTreeTest extends SapphireTest {
// Check that if we restore while on the live site that the content still gets pushed to
// stage
Versioned::reading_stage('Live');
Versioned::set_stage(Versioned::LIVE);
$deletedPage = Versioned::get_latest_version('SiteTree', $page2ID);
$deletedPage->doRestoreToStage();
$this->assertFalse((bool)Versioned::get_one_by_stage("Page", "Live", "\"SiteTree\".\"ID\" = " . $page2ID));
Versioned::reading_stage('Stage');
Versioned::set_stage(Versioned::DRAFT);
$requeriedPage = DataObject::get_by_id("Page", $page2ID);
$this->assertEquals('Products', $requeriedPage->Title);
$this->assertEquals('Page', $requeriedPage->class);
@ -383,12 +383,12 @@ class SiteTreeTest extends SapphireTest {
$parentPage->doUnpublish();
Versioned::reading_stage('Live');
Versioned::set_stage(Versioned::LIVE);
$this->assertFalse(DataObject::get_by_id('Page', $pageAbout->ID));
$this->assertTrue(DataObject::get_by_id('Page', $pageStaff->ID) instanceof Page);
$this->assertTrue(DataObject::get_by_id('Page', $pageStaffDuplicate->ID) instanceof Page);
Versioned::reading_stage('Stage');
Versioned::set_stage(Versioned::DRAFT);
Config::inst()->update('SiteTree', 'enforce_strict_hierarchy', true);
}
@ -406,11 +406,11 @@ class SiteTreeTest extends SapphireTest {
$parentPage = $this->objFromFixture('Page', 'about');
$parentPage->doUnpublish();
Versioned::reading_stage('Live');
Versioned::set_stage(Versioned::LIVE);
$this->assertFalse(DataObject::get_by_id('Page', $pageAbout->ID));
$this->assertTrue(DataObject::get_by_id('Page', $pageStaff->ID) instanceof Page);
$this->assertTrue(DataObject::get_by_id('Page', $pageStaffDuplicate->ID) instanceof Page);
Versioned::reading_stage('Stage');
Versioned::set_stage(Versioned::DRAFT);
Config::inst()->update('SiteTree', 'enforce_strict_hierarchy', true);
}
@ -427,11 +427,11 @@ class SiteTreeTest extends SapphireTest {
$parentPage = $this->objFromFixture('Page', 'about');
$parentPage->doUnpublish();
Versioned::reading_stage('Live');
Versioned::set_stage(Versioned::LIVE);
$this->assertFalse(DataObject::get_by_id('Page', $pageAbout->ID));
$this->assertFalse(DataObject::get_by_id('Page', $pageStaff->ID));
$this->assertFalse(DataObject::get_by_id('Page', $pageStaffDuplicate->ID));
Versioned::reading_stage('Stage');
Versioned::set_stage(Versioned::DRAFT);
}
/**
@ -1115,7 +1115,7 @@ class SiteTreeTest extends SapphireTest {
$member->Groups()->add($group);
// both pages are viewable in stage
Versioned::reading_stage('Stage');
Versioned::set_stage(Versioned::DRAFT);
$about = $this->objFromFixture('Page', 'about');
$staff = $this->objFromFixture('Page', 'staff');
$this->assertFalse($about->isOrphaned());
@ -1127,16 +1127,16 @@ class SiteTreeTest extends SapphireTest {
$staff->publish('Stage', 'Live');
$this->assertFalse($staff->isOrphaned());
$this->assertTrue($staff->canView($member));
Versioned::reading_stage('Live');
Versioned::set_stage(Versioned::LIVE);
$staff = $this->objFromFixture('Page', 'staff'); // Live copy of page
$this->assertTrue($staff->isOrphaned()); // because parent isn't published
$this->assertFalse($staff->canView($member));
// Publishing the parent page should restore visibility
Versioned::reading_stage('Stage');
Versioned::set_stage(Versioned::DRAFT);
$about = $this->objFromFixture('Page', 'about');
$about->publish('Stage', 'Live');
Versioned::reading_stage('Live');
Versioned::set_stage(Versioned::LIVE);
$staff = $this->objFromFixture('Page', 'staff');
$this->assertFalse($staff->isOrphaned());
$this->assertTrue($staff->canView($member));

View File

@ -30,28 +30,9 @@ class VirtualPageTest extends FunctionalTest {
// Ensure we always have permission to save/publish
$this->logInWithPermission("ADMIN");
$this->origInitiallyCopiedFields = VirtualPage::config()->initially_copied_fields;
Config::inst()->remove('VirtualPage', 'initially_copied_fields');
VirtualPage::config()->initially_copied_fields = array_merge(
$this->origInitiallyCopiedFields,
array('MyInitiallyCopiedField')
);
$this->origNonVirtualField = VirtualPage::config()->non_virtual_fields;
Config::inst()->remove('VirtualPage', 'non_virtual_fields');
VirtualPage::config()->non_virtual_fields = array_merge(
$this->origNonVirtualField,
array('MyNonVirtualField', 'MySharedNonVirtualField')
);
}
public function tearDown() {
parent::tearDown();
Config::inst()->remove('VirtualPage', 'initially_copied_fields');
Config::inst()->remove('VirtualPage', 'non_virtual_fields');
VirtualPage::config()->initially_copied_fields = $this->origInitiallyCopiedFields;
VirtualPage::config()->non_virtual_fields = $this->origNonVirtualField;
// Add extra fields
Config::inst()->update('VirtualPage', 'initially_copied_fields', array('MyInitiallyCopiedField'));
Config::inst()->update('VirtualPage', 'non_virtual_fields', array('MyNonVirtualField', 'MySharedNonVirtualField'));
}
/**
@ -98,7 +79,7 @@ class VirtualPageTest extends FunctionalTest {
$master->doPublish();
Versioned::reading_stage("Live");
Versioned::set_stage(Versioned::LIVE);
$vp1 = DataObject::get_by_id("VirtualPage", $this->idFromFixture('VirtualPage', 'vp1'));
$vp2 = DataObject::get_by_id("VirtualPage", $this->idFromFixture('VirtualPage', 'vp2'));
@ -111,7 +92,7 @@ class VirtualPageTest extends FunctionalTest {
$this->assertEquals("New menutitle", $vp2->MenuTitle);
$this->assertEquals("<p>New content</p>", $vp1->Content);
$this->assertEquals("<p>New content</p>", $vp2->Content);
Versioned::reading_stage("Stage");
Versioned::set_stage(Versioned::DRAFT);
}
/**
@ -145,23 +126,36 @@ class VirtualPageTest extends FunctionalTest {
$p->write();
$p->doPublish();
// Don't publish this change - published page will still say 'published content'
$p->Content = "draft content";
$p->write();
// Virtual page has this content
$vp = new VirtualPage();
$vp->CopyContentFromID = $p->ID;
$vp->write();
$vp->doPublish();
// Don't publish this change - published page will still say 'published content'
$p->Content = "draft content";
$p->write();
// The draft content of the virtual page should say 'draft content'
$this->assertEquals('draft content',
DB::query('SELECT "Content" from "SiteTree" WHERE "ID" = ' . $vp->ID)->value());
/** @var VirtualPage $vpDraft */
$vpDraft = Versioned::get_by_stage("VirtualPage", Versioned::DRAFT)->byID($vp->ID);
$this->assertEquals('draft content', $vpDraft->CopyContentFrom()->Content);
$this->assertEquals('draft content', $vpDraft->Content);
// The published content of the virtual page should say 'published content'
$this->assertEquals('published content',
DB::query('SELECT "Content" from "SiteTree_Live" WHERE "ID" = ' . $vp->ID)->value());
/** @var VirtualPage $vpLive */
$vpLive = Versioned::get_by_stage("VirtualPage", Versioned::LIVE)->byID($vp->ID);
$this->assertEquals('published content', $vpLive->CopyContentFrom()->Content);
$this->assertEquals('published content', $vpLive->Content);
// Publishing the virtualpage should, however, trigger publishing of the live page
$vpDraft->doPublish();
// Everything is published live
$vpLive = Versioned::get_by_stage("VirtualPage", Versioned::LIVE)->byID($vp->ID);
$this->assertEquals('draft content', $vpLive->CopyContentFrom()->Content);
$this->assertEquals('draft content', $vpLive->Content);
}
public function testCantPublishVirtualPagesBeforeTheirSource() {
@ -191,22 +185,36 @@ class VirtualPageTest extends FunctionalTest {
$p->Content = "test content";
$p->write();
$p->doPublish();
$pID = $p->ID;
$vp = new VirtualPage();
$vp->CopyContentFromID = $p->ID;
$vp->write();
// Delete the source page
$this->assertTrue($vp->canPublish());
$this->assertTrue($p->doUnpublish());
$this->assertTrue($vp->doPublish());
// Delete the source page semi-manually, without triggering
// the cascade publish back to the virtual page.
Versioned::set_stage(Versioned::LIVE);
$livePage = Versioned::get_by_stage('SiteTree', Versioned::LIVE)->byID($pID);
$livePage->delete();
Versioned::set_stage(Versioned::DRAFT);
// Confirm that we can unpublish, but not publish
$this->assertFalse($p->IsPublished(), 'Copied page has orphaned the virtual page on live');
$this->assertTrue($vp->isPublished(), 'Virtual page remains on live');
$this->assertTrue($vp->canUnpublish());
$this->assertFalse($vp->canPublish());
// Confirm that the action really works
$this->assertTrue($vp->doUnpublish());
$this->assertNull(DB::query("SELECT \"ID\" FROM \"SiteTree_Live\" WHERE \"ID\" = $vp->ID")->value());
$this->assertEquals(
0,
DB::prepared_query(
"SELECT count(*) FROM \"SiteTree_Live\" WHERE \"ID\" = ?",
array($vp->ID)
)->value()
);
}
public function testCanEdit() {
@ -259,7 +267,6 @@ class VirtualPageTest extends FunctionalTest {
// VP is still orange after we publish
$p->doPublish();
$this->fixVersionNumberCache($vp);
$this->assertTrue($vp->getIsAddedToStage());
// A new VP created after P's initial construction
@ -272,77 +279,28 @@ class VirtualPageTest extends FunctionalTest {
$p->Content = "new content";
$p->write();
$p->doPublish();
$this->fixVersionNumberCache($vp2);
$this->assertTrue($vp2->getIsAddedToStage());
// VP is now published
$vp->doPublish();
$this->fixVersionNumberCache($vp);
$this->assertTrue($vp->getExistsOnLive());
$this->assertFalse($vp->getIsModifiedOnStage());
// P edited, VP and P both go green
// P edited, P goes green. Change set interface should indicate to the user that the owned page has
// modifications, although the virtual page record itself will not appear as having pending changes.
$p->Content = "third content";
$p->write();
$this->fixVersionNumberCache($vp, $p);
$this->assertTrue($p->getIsModifiedOnStage());
$this->assertTrue($vp->getIsModifiedOnStage());
$this->assertFalse($vp->getIsModifiedOnStage());
// Publish, VP goes black
$p->doPublish();
$this->fixVersionNumberCache($vp);
$this->assertTrue($vp->getExistsOnLive());
$this->assertFalse($vp->getIsModifiedOnStage());
}
public function testVirtualPagesCreateVersionRecords() {
$source = $this->objFromFixture('Page', 'master');
$source->Title = "T0";
$source->write();
$source->doPublish();
// Creating a new VP to ensure that Version #s are out of alignment
$vp = new VirtualPage();
$vp->CopyContentFromID = $source->ID;
$vp->write();
$source->Title = "T1";
$source->write();
$source->Title = "T2";
$source->write();
$this->assertEquals($vp->ID, DB::query("SELECT \"RecordID\" FROM \"SiteTree_versions\"
WHERE \"RecordID\" = $vp->ID AND \"Title\" = 'T1'")->value());
$this->assertEquals($vp->ID, DB::query("SELECT \"RecordID\" FROM \"SiteTree_versions\"
WHERE \"RecordID\" = $vp->ID AND \"Title\" = 'T2'")->value());
$this->assertEquals($vp->ID, DB::query("SELECT \"RecordID\" FROM \"SiteTree_versions\"
WHERE \"RecordID\" = $vp->ID AND \"Version\" = $vp->Version")->value());
$vp->doPublish();
// Check that the published content is copied from the published page, with a legal
// version
$liveVersion = DB::query("SELECT \"Version\" FROM \"SiteTree_Live\" WHERE \"ID\" = $vp->ID")->value();
$this->assertEquals("T0", DB::query("SELECT \"Title\" FROM \"SiteTree_Live\"
WHERE \"ID\" = $vp->ID")->value());
// SiteTree_Live.Version should reference a legal entry in SiteTree_versions for the
// virtual page
$this->assertEquals("T0", DB::query("SELECT \"Title\" FROM \"SiteTree_versions\"
WHERE \"RecordID\" = $vp->ID AND \"Version\" = $liveVersion")->value());
}
public function fixVersionNumberCache($page) {
$pages = func_get_args();
foreach($pages as $p) {
Versioned::prepopulate_versionnumber_cache('SiteTree', 'Stage', array($p->ID));
Versioned::prepopulate_versionnumber_cache('SiteTree', 'Live', array($p->ID));
}
}
public function testUnpublishingSourcePageOfAVirtualPageAlsoUnpublishesVirtualPage() {
// Create page and virutal page
$p = new Page();
@ -459,7 +417,6 @@ class VirtualPageTest extends FunctionalTest {
$virtual->CopyContentFromID = $original->ID;
$virtual->write();
$virtual->copyFrom($original);
// Using getField() to avoid side effects from an overloaded __get()
$this->assertEquals(
'original',
@ -478,7 +435,6 @@ class VirtualPageTest extends FunctionalTest {
$original->MyInitiallyCopiedField = 'changed';
$original->write();
$virtual->copyFrom($original);
$this->assertEquals(
'original',
$virtual->MyInitiallyCopiedField,
@ -486,53 +442,6 @@ class VirtualPageTest extends FunctionalTest {
);
}
public function testWriteWithoutVersion() {
$original = new SiteTree();
$original->write();
// Create a second version (different behaviour),
// as SiteTree->onAfterWrite() checks for Version == 1
$original->Title = 'prepare';
$original->write();
$originalVersion = $original->Version;
$virtual = new VirtualPage();
$virtual->CopyContentFromID = $original->ID;
$virtual->write();
// Create a second version, see above.
$virtual->Title = 'prepare';
$virtual->write();
$virtualVersion = $virtual->Version;
$virtual->Title = 'changed 1';
$virtual->writeWithoutVersion();
$this->assertEquals(
$virtual->Version,
$virtualVersion,
'writeWithoutVersion() on VirtualPage doesnt increment version'
);
$original->Title = 'changed 2';
$original->writeWithoutVersion();
DataObject::flush_and_destroy_cache();
$virtual = DataObject::get_by_id('VirtualPage', $virtual->ID, false);
$this->assertEquals(
$virtual->Version,
$virtualVersion,
'writeWithoutVersion() on original page doesnt increment version on related VirtualPage'
);
$original->Title = 'changed 3';
$original->write();
DataObject::flush_and_destroy_cache();
$virtual = DataObject::get_by_id('VirtualPage', $virtual->ID, false);
$this->assertGreaterThan(
$virtualVersion,
$virtual->Version,
'write() on original page does increment version on related VirtualPage'
);
}
public function testCanBeRoot() {
$page = new SiteTree();
$page->ParentID = 0;
@ -562,50 +471,16 @@ class VirtualPageTest extends FunctionalTest {
if(!$isDetected) $this->fail('Fails validation with $can_be_root=false');
}
public function testPageTypeChangeDoesntKeepOrphanedVirtualPageRecord() {
$page = new SiteTree();
$page->write();
$page->publish('Stage', 'Live');
$virtual = new VirtualPageTest_VirtualPageSub();
$virtual->CopyContentFromID = $page->ID;
$virtual->write();
$virtual->publish('Stage', 'Live');
$nonVirtual = $virtual;
$nonVirtual->ClassName = 'VirtualPageTest_ClassA';
$nonVirtual->write(); // not publishing
$this->assertNotNull(
DB::query(sprintf('SELECT "ID" FROM "SiteTree" WHERE "ID" = %d', $nonVirtual->ID))->value(),
"Shared base database table entry exists after type change"
);
$this->assertNull(
DB::query(sprintf('SELECT "ID" FROM "VirtualPage" WHERE "ID" = %d', $nonVirtual->ID))->value(),
"Base database table entry no longer exists after type change"
);
$this->assertNull(
DB::query(sprintf('SELECT "ID" FROM "VirtualPageTest_VirtualPageSub" WHERE "ID" = %d', $nonVirtual->ID))->value(),
"Sub database table entry no longer exists after type change"
);
$this->assertNull(
DB::query(sprintf('SELECT "ID" FROM "VirtualPage_Live" WHERE "ID" = %d', $nonVirtual->ID))->value(),
"Base live database table entry no longer exists after type change"
);
$this->assertNull(
DB::query(sprintf('SELECT "ID" FROM "VirtualPageTest_VirtualPageSub_Live" WHERE "ID" = %d', $nonVirtual->ID))->value(),
"Sub live database table entry no longer exists after type change"
);
}
public function testPageTypeChangePropagatesToLive() {
$page = new SiteTree();
$page->Title = 'published title';
$page->MySharedNonVirtualField = 'original';
$page->write();
$page->publish('Stage', 'Live');
$virtual = new VirtualPageTest_VirtualPageSub();
$virtual->CopyContentFromID = $page->ID;
$virtual->MySharedNonVirtualField = 'virtual published field';
$virtual->write();
$virtual->publish('Stage', 'Live');
@ -614,28 +489,38 @@ class VirtualPageTest extends FunctionalTest {
// but we want to test that it gets copied on class name change instead
$page->write();
$nonVirtual = $virtual;
$nonVirtual->ClassName = 'VirtualPageTest_ClassA';
$nonVirtual->MySharedNonVirtualField = 'changed on new type';
$nonVirtual->write(); // not publishing the page type change here
$this->assertEquals('original', $nonVirtual->Title,
// Stage record is changed to the new type and no longer acts as a virtual page
$nonVirtualStage = Versioned::get_one_by_stage('SiteTree', 'Stage', '"SiteTree"."ID" = ' . $nonVirtual->ID, false);
$this->assertNotNull($nonVirtualStage);
$this->assertEquals('VirtualPageTest_ClassA', $nonVirtualStage->ClassName);
$this->assertEquals('changed on new type', $nonVirtualStage->MySharedNonVirtualField);
$this->assertEquals('original', $nonVirtualStage->Title,
'Copies virtual fields from original draft into new instance on type change '
);
$nonVirtualLive = Versioned::get_one_by_stage('SiteTree', 'Live', '"SiteTree_Live"."ID" = ' . $nonVirtual->ID);
$this->assertNotNull($nonVirtualLive);
$this->assertEquals('VirtualPageTest_ClassA', $nonVirtualLive->ClassName);
$this->assertEquals('changed on new type', $nonVirtualLive->MySharedNonVirtualField);
// Virtual page on live keeps working as it should
$virtualLive = Versioned::get_one_by_stage('SiteTree', 'Live', '"SiteTree_Live"."ID" = ' . $virtual->ID, false);
$this->assertNotNull($virtualLive);
$this->assertEquals('VirtualPageTest_VirtualPageSub', $virtualLive->ClassName);
$this->assertEquals('virtual published field', $virtualLive->MySharedNonVirtualField);
$this->assertEquals('published title', $virtualLive->Title);
// Change live page
$page->Title = 'title changed on original';
$page->MySharedNonVirtualField = 'changed only on original';
$page->write();
$page->publish('Stage', 'Live');
$nonVirtualLive = Versioned::get_one_by_stage('SiteTree', 'Live', '"SiteTree_Live"."ID" = ' . $nonVirtual->ID, false);
$this->assertEquals('changed on new type', $nonVirtualLive->MySharedNonVirtualField,
'No field copying from previous original after page type changed'
);
// Virtual page only notices changes to virtualised fields (Title)
$virtualLive = Versioned::get_one_by_stage('SiteTree', 'Live', '"SiteTree_Live"."ID" = ' . $virtual->ID, false);
$this->assertEquals('virtual published field', $virtualLive->MySharedNonVirtualField);
$this->assertEquals('title changed on original', $virtualLive->Title);
}
public function testVirtualPageFindsCorrectCasting() {