initially_copied_fields} is implicitly included in this list. */ private static $non_virtual_fields = [ "ID", "ClassName", "ObsoleteClassName", "SecurityTypeID", "OwnerID", "ParentID", "URLSegment", "Sort", "Status", 'ShowInMenus', // 'Locale' 'ShowInSearch', 'Version', "Embargo", "Expiry", "CanViewType", "CanEditType", "CopyContentFromID", "HasBrokenLink", ]; /** * @var array Define fields that are initially copied to virtual pages but left modifiable after that. */ private static $initially_copied_fields = [ 'ShowInMenus', 'ShowInSearch', 'URLSegment', ]; private static $has_one = [ "CopyContentFrom" => SiteTree::class, ]; private static $owns = [ "CopyContentFrom", ]; private static $db = [ "VersionID" => "Int", ]; private static $table_name = 'VirtualPage'; /** * Generates the array of fields required for the page type. * * @return array */ public function getVirtualFields() { // Check if copied page exists $record = $this->CopyContentFrom(); if (!$record || !$record->exists()) { return []; } // Diff db with non-virtual fields $fields = array_keys(static::getSchema()->fieldSpecs($record) ?? []); $nonVirtualFields = $this->getNonVirtualisedFields(); return array_diff($fields ?? [], $nonVirtualFields); } /** * List of fields or properties to never virtualise * * @return array */ public function getNonVirtualisedFields() { $config = self::config(); return array_merge( $config->get('non_virtual_fields'), $config->get('initially_copied_fields') ); } public function setCopyContentFromID($val) { // Sanity check to prevent pages virtualising other virtual pages if ($val && DataObject::get_by_id(SiteTree::class, $val) instanceof VirtualPage) { $val = 0; } return $this->setField("CopyContentFromID", $val); } public function ContentSource() { $copied = $this->CopyContentFrom(); if ($copied && $copied->exists()) { return $copied; } return $this; } /** * @return array */ public function MetaComponents() { $tags = parent::MetaComponents(); $copied = $this->CopyContentFrom(); if ($copied && $copied->exists()) { $tags['canonical'] = [ 'tag' => 'link', 'attributes' => [ 'rel' => 'canonical', 'href' => $copied->AbsoluteLink(), ], ]; } return $tags; } public function allowedChildren() { $copy = $this->CopyContentFrom(); if ($copy && $copy->exists()) { return $copy->allowedChildren(); } return []; } public function syncLinkTracking() { if ($this->CopyContentFromID) { $this->HasBrokenLink = Versioned::get_by_stage(SiteTree::class, Versioned::DRAFT) ->filter('ID', $this->CopyContentFromID) ->count() === 0; } else { $this->HasBrokenLink = true; } } /** * 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); } /** * Returns true if is page is publishable by anyone at all * Return false if the source page isn't published yet. * * Note that isPublishable doesn't affect ete from live, only publish. */ public function isPublishable() { // No source if (!$this->CopyContentFrom() || !$this->CopyContentFrom()->ID) { return false; } // Unpublished source if (!Versioned::get_versionnumber_by_stage( SiteTree::class, 'Live', $this->CopyContentFromID )) { return false; } // Default - publishable return true; } /** * Generate the CMS fields from the fields from the original page. */ public function getCMSFields() { $this->beforeUpdateCMSFields(function (FieldList $fields) { // Setup the linking to the original page. $copyContentFromField = TreeDropdownField::create( 'CopyContentFromID', _t(self::class . '.CHOOSE', "Linked Page"), SiteTree::class ); // Setup virtual fields if ($virtualFields = $this->getVirtualFields()) { $roTransformation = new ReadonlyTransformation(); foreach ($virtualFields as $virtualField) { if ($fields->dataFieldByName($virtualField)) { $fields->replaceField( $virtualField, $fields->dataFieldByName($virtualField)->transform($roTransformation) ); } } } $msgs = []; $fields->addFieldToTab('Root.Main', $copyContentFromField, 'Title'); // Create links back to the original object in the CMS if ($this->CopyContentFrom()->exists()) { $link = HTML::createTag( 'a', [ 'class' => 'cmsEditlink', 'href' => $this->CopyContentFrom()->CMSEditLink(), ], _t(self::class . '.EditLink', 'edit') ); $msgs[] = _t( self::class . '.HEADERWITHLINK', "This is a virtual page copying content from \"{title}\" ({link})", [ 'title' => $this->CopyContentFrom()->obj('Title'), 'link' => $link, ] ); } else { $msgs[] = _t(self::class . '.HEADER', "This is a virtual page"); $msgs[] = _t( 'SilverStripe\\CMS\\Model\\SiteTree.VIRTUALPAGEWARNING', 'Please choose a linked page and save first in order to publish this page' ); } if ($this->CopyContentFromID && !Versioned::get_versionnumber_by_stage( SiteTree::class, Versioned::LIVE, $this->CopyContentFromID )) { $msgs[] = _t( 'SilverStripe\\CMS\\Model\\SiteTree.VIRTUALPAGEDRAFTWARNING', 'Please publish the linked page in order to publish the virtual page' ); } $fields->addFieldToTab("Root.Main", new LiteralField( 'VirtualPageMessage', '