Version: current

      1 <?php
      2 /**
      3 * Virtual Page creates an instance of a  page, with the same fields that the original page had, but readonly.
      4 * This allows you can have a page in mulitple places in the site structure, with different children without duplicating the content
      5 * Note: This Only duplicates $db fields and not the $has_one etc..
      6 * @package cms
      7 */
      8 class VirtualPage extends Page {
      9
     10     static $icon = array("cms/images/treeicons/page-shortcut-gold","file");
     11
     12     public static $virtualFields;
     13
     14     static $has_one = array(
     15         "CopyContentFrom" => "SiteTree",
     16     );
     17
     18     static $db = array(
     19         "VersionID" => "Int",
     20     );
     21
     22     /**
     23      * Generates the array of fields required for the page type.
     24      */
     25     function getVirtualFields() {
     26         $nonVirtualFields = array(
     27             "SecurityTypeID",
     28             "OwnerID",
     29             "URLSegment",
     30             "Sort",
     31             "Status",
     32             'ShowInMenus',
     33             // 'Locale'
     34             'ShowInSearch',
     35             'Version',
     36             "Embargo",
     37             "Expiry",
     38         );
     39
     40         $allFields = $this->db();
     41         if($hasOne = $this->has_one()) foreach($hasOne as $link) $allFields[$link . 'ID'] = "Int";
     42         foreach($allFields as $field => $type) {
     43             if(!in_array($field, $nonVirtualFields)) $virtualFields[] = $field;
     44         }
     45
     46         return $virtualFields;
     47     }
     48
     49     function CopyContentFrom() {
     50         if(empty($this->record['CopyContentFromID'])) return new SiteTree();
     51
     52         if(!isset($this->components['CopyContentFrom'])) {
     53             $this->components['CopyContentFrom'] = DataObject::get_by_id("SiteTree",
     54                 $this->record['CopyContentFromID']);
     55
     56             // Don't let VirtualPages point to other VirtualPages
     57             if($this->components['CopyContentFrom'] instanceof VirtualPage) {
     58                 $this->components['CopyContentFrom'] = null;
     59             }
     60
     61             // has_one component semantics incidate than an empty object should be returned
     62             if(!$this->components['CopyContentFrom']) {
     63                 $this->components['CopyContentFrom'] = new SiteTree();
     64             }
     65         }
     66
     67         return $this->components['CopyContentFrom'];
     68     }
     69     function setCopyContentFromID($val) {
     70         if(DataObject::get_by_id('SiteTree', (int)$val) instanceof VirtualPage) $val = 0;
     71         return $this->setField("CopyContentFromID", $val);
     72     }
     73
     74     function ContentSource() {
     75         return $this->CopyContentFrom();
     76     }
     77
     78     function allowedChildren() {
     79         if($this->CopyContentFrom()) {
     80             return $this->CopyContentFrom()->allowedChildren();
     81         }
     82     }
     83
     84     public function syncLinkTracking() {
     85         if($this->CopyContentFromID) {
     86             $this->HasBrokenLink = !(bool) DataObject::get_by_id('SiteTree', $this->CopyContentFromID);
     87         } else {
     88             $this->HasBrokenLink = true;
     89         }
     90     }
     91
     92     /**
     93      * We can only publish the page if there is a published source page
     94      */
     95     public function canPublish($member = null) {
     96         return $this->isPublishable() && parent::canPublish($member);
     97     }
     98
     99     /**
    100      * Return true if we can delete this page from the live site, which is different from can
    101      * we publish it.
    102      */
    103     public function canDeleteFromLive($member = null) {
    104         return parent::canPublish($member);
    105     }
    106
    107     /**
    108      * Returns true if is page is publishable by anyone at all
    109      * Return false if the source page isn't published yet.
    110      *
    111      * Note that isPublishable doesn't affect ete from live, only publish.
    112      */
    113     public function isPublishable() {
    114         // No source
    115         if(!$this->CopyContentFrom() || !$this->CopyContentFrom()->ID) {
    116             return false;
    117         }
    118
    119         // Unpublished source
    120         if(!Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->CopyContentFromID)) {
    121             return false;
    122         }
    123
    124         // Default - publishable
    125         return true;
    126     }
    127
    128     /**
    129      * Generate the CMS fields from the fields from the original page.
    130      */
    131     function getCMSFields($cms = null) {
    132         $fields = parent::getCMSFields($cms);
    133
    134         // Setup the linking to the original page.
    135         $copyContentFromField = new TreeDropdownField(
    136             "CopyContentFromID",
    137             _t('VirtualPage.CHOOSE', "Choose a page to link to"),
    138             "SiteTree"
    139         );
    140         // filter doesn't let you select children of virtual pages as as source page
    141         //$copyContentFromField->setFilterFunction(create_function('$item', 'return !($item instanceof VirtualPage);'));
    142
    143         // Setup virtual fields
    144         if($virtualFields = $this->getVirtualFields()) {
    145             $roTransformation = new ReadonlyTransformation();
    146             foreach($virtualFields as $virtualField) {
    147                 if($fields->dataFieldByName($virtualField))
    148                     $fields->replaceField($virtualField, $fields->dataFieldByName($virtualField)->transform($roTransformation));
    149             }
    150         }
    151
    152         // Add fields to the tab
    153         $fields->addFieldToTab("Root.Content.Main",
    154             new HeaderField('VirtualPageHeader',_t('VirtualPage.HEADER', "This is a virtual page")),
    155             "Title"
    156         );
    157         $fields->addFieldToTab("Root.Content.Main", $copyContentFromField, "Title");
    158
    159         // Create links back to the original object in the CMS
    160         if($this->CopyContentFrom()->ID) {
    161             $linkToContent = "<a class=\"cmsEditlink\" href=\"admin/show/$this->CopyContentFromID\">" .
    162                 _t('VirtualPage.EDITCONTENT', 'click here to edit the content') . "</a>";
    163             $fields->addFieldToTab("Root.Content.Main",
    164                 $linkToContentLabelField = new LabelField('VirtualPageContentLinkLabel', $linkToContent),
    165                 "Title"
    166             );
    167             $linkToContentLabelField->setAllowHTML(true);
    168         }
    169
    170         return $fields;
    171     }
    172
    173     /**
    174      * We have to change it to copy all the content from the original page first.
    175      */
    176     function onBeforeWrite() {
    177         // On regular write, this will copy from published source.  This happens on every publish
    178         if($this->extension_instances['Versioned']->migratingVersion
    179             && Versioned::current_stage() == 'Live') {
    180             if($this->CopyContentFromID) {
    181                 $performCopyFrom = true;
    182
    183             $stageSourceVersion = DB::query("SELECT \"Version\" FROM \"SiteTree\" WHERE \"ID\" = $this->CopyContentFromID")->value();
    184             $liveSourceVersion = DB::query("SELECT \"Version\" FROM \"SiteTree_Live\" WHERE \"ID\" = $this->CopyContentFromID")->value();
    185
    186                 // We're going to create a new VP record in SiteTree_versions because the published
    187                 // version might not exist, unless we're publishing the latest version
    188                 if($stageSourceVersion != $liveSourceVersion) {
    189                     $this->extension_instances['Versioned']->migratingVersion = null;
    190                 }
    191             }
    192
    193         // On regular write, this will copy from draft source.  This is only executed when the source
    194         // page changeds
    195         } else {
    196             $performCopyFrom = $this->isChanged('CopyContentFromID') && $this->CopyContentFromID != 0;
    197         }
    198
    199         // On publish, this will copy from published source
    200         if($performCopyFrom && $this instanceof VirtualPage) {
    201             // This flush is needed because the get_one cache doesn't respect site version :-(
    202             singleton('SiteTree')->flushCache();
    203             $source = DataObject::get_one("SiteTree",sprintf('"SiteTree"."ID" = %d', $this->CopyContentFromID));
    204             // Leave the updating of image tracking until after write, in case its a new record
    205             $this->copyFrom($source, false);
    206             $this->URLSegment = $source->URLSegment;
    207         }
    208
    209         parent::onBeforeWrite();
    210     }
    211
    212     function onAfterWrite() {
    213         parent::onAfterWrite();
    214
    215         // Don't do this stuff when we're publishing
    216         if(!$this->extension_instances['Versioned']->migratingVersion) {
    217             if(
    218                 $this->isChanged('CopyContentFromID')
    219                 && $this->CopyContentFromID != 0
    220                 && $this instanceof VirtualPage
    221             ) {
    222                 $this->updateImageTracking();
    223             }
    224         }
    225
    226         FormResponse::add("$('Form_EditForm').reloadIfSetTo($this->ID);", $this->ID."_VirtualPage_onAfterWrite");
    227     }
    228
    229     /**
    230      * Ensure we have an up-to-date version of everything.
    231      */
    232     function copyFrom($source, $updateImageTracking = true) {
    233         if($source) {
    234             foreach($this->getVirtualFields() as $virtualField) {
    235                 $this->$virtualField = $source->$virtualField;
    236             }
    237
    238             // We also want to copy ShowInMenus, but only if we're copying the
    239             // source page for the first time.
    240             if($this->isChanged('CopyContentFromID')) {
    241                 $this->ShowInMenus = $source->ShowInMenus;
    242             }
    243
    244             if($updateImageTracking) $this->updateImageTracking();
    245         }
    246     }
    247
    248     function updateImageTracking() {
    249         // Doesn't work on unsaved records
    250         if(!$this->ID) return;
    251
    252         // Remove CopyContentFrom() from the cache
    253         unset($this->components['CopyContentFrom']);
    254
    255         // Update ImageTracking
    256         $ID = $this->CopyContentFrom()->ID;
    257         $class = $this->CopyContentFrom()->ClassName;
    258
    259
    260         if ($copyContentFromTracking = DataObjectFileTracking::get_track($ID, $class)) { // берем полный трекинг CopyContentFrom-страницы
    261             $trackingData = array();
    262             foreach($copyContentFromTracking as $entry) {
    263                 if (isset($trackingData[$entry->FieldName])) {
    264                     $trackingData[$entry->FieldName][] = $entry->FileID;
    265                 } else {
    266                     $trackingData[$entry->FieldName] = array($entry->FileID);
    267                 }
    268             }
    269
    270             // трекаем только те поля VirtualPage, которые в ней непосредственно заведены
    271             $virtualFields = $this->getVirtualFields();
    272             foreach($trackingData as $fieldName=>$IDs) {
    273                 if (in_array($fieldName, $virtualFields)) {
    274                     DataObjectFileTracking::update_track($this->ID, $this->ClassName, $fieldName, $IDs);
    275                 }
    276             }
    277         }
    278     }
    279
    280     /**
    281      * Allow attributes on the master page to pass
    282      * through to the virtual page
    283      *
    284      * @param string $field
    285      * @return mixed
    286      */
    287     function __get($field) {
    288         if(parent::hasMethod($funcName = "get$field")) {
    289             return $this->$funcName();
    290         } else if(parent::hasField($field)) {
    291             return $this->getField($field);
    292         } else {
    293             return $this->copyContentFrom()->$field;
    294         }
    295     }
    296
    297     /**
    298      * Pass unrecognized method calls on to the original data object
    299      *
    300      * @param string $method
    301      * @param string $args
    302      */
    303     function __call($method, $args) {
    304         if(parent::hasMethod($method)) {
    305             return parent::__call($method, $args);
    306         } else {
    307             return call_user_func_array(array($this->copyContentFrom(), $method), $args);
    308         }
    309     }
    310
    311     public function hasField($field) {
    312         return (
    313             array_key_exists($field, $this->record)
    314             || $this->hasDatabaseField($field)
    315             || array_key_exists($field, $this->db()) // Needed for composite fields
    316             || parent::hasMethod("get{$field}")
    317             || $this->CopyContentFrom()->hasField($field)
    318         );
    319     }
    320     /**
    321      * Overwrite to also check for method on the original data object
    322      *
    323      * @param string $method
    324      * @return bool
    325      */
    326     function hasMethod($method) {
    327         if(parent::hasMethod($method)) return true;
    328         return $this->copyContentFrom()->hasMethod($method);
    329     }
    330 }
    331
    332 /**
    333  * Controller for the virtual page.
    334  * @package cms
    335  */
    336 class VirtualPage_Controller extends Page_Controller {
    337
    338     static $allowed_actions = array(
    339         'loadcontentall' => 'ADMIN',
    340     );
    341
    342     /**
    343      * Reloads the content if the version is different ;-)
    344      */
    345     function reloadContent() {
    346         $this->failover->copyFrom($this->failover->CopyContentFrom());
    347         $this->failover->write();
    348         return;
    349     }
    350
    351     function getViewer($action) {
    352         $originalClass = get_class($this->CopyContentFrom());
    353         if ($originalClass == 'SiteTree') $name = 'Page_Controller';
    354         else $name = $originalClass."_Controller";
    355         $controller = new $name();
    356         return $controller->getViewer($action);
    357     }
    358
    359     /**
    360      * When the virtualpage is loaded, check to see if the versions are the same
    361      * if not, reload the content.
    362      * NOTE: Virtual page must have a container object of subclass of sitetree.
    363      * We can't load the content without an ID or record to copy it from.
    364      */
    365     function init(){
    366         if(isset($this->record) && $this->record->ID){
    367             if($this->record->VersionID != $this->failover->CopyContentFrom()->Version){
    368                 $this->reloadContent();
    369                 $this->VersionID = $this->failover->CopyContentFrom()->VersionID;
    370             }
    371         }
    372         parent::init();
    373     }
    374
    375     function loadcontentall() {
    376         $pages = DataObject::get("VirtualPage");
    377         foreach($pages as $page) {
    378             $page->copyFrom($page->CopyContentFrom());
    379             $page->write();
    380             $page->publish("Stage", "Live");
    381             echo "<li>Published $page->URLSegment";
    382         }
    383     }
    384
    385     /**
    386      * Also check the original object's original controller for the method
    387      *
    388      * @param string $method
    389      * @return bool
    390      */
    391     function hasMethod($method) {
    392         $haveIt = parent::hasMethod($method);
    393         if (!$haveIt) {
    394             $originalClass = get_class($this->CopyContentFrom());
    395             if ($originalClass == 'SiteTree') $name = 'ContentController';
    396             else $name = $originalClass."_Controller";
    397             $controller = new $name($this->dataRecord->copyContentFrom());
    398             $haveIt = $controller->hasMethod($method);
    399         }
    400         return $haveIt;
    401     }
    402
    403     /**
    404      * Pass unrecognized method calls on to the original controller
    405      *
    406      * @param string $method
    407      * @param string $args
    408      */
    409     function __call($method, $args) {
    410         try {
    411             return parent::__call($method, $args);
    412         } catch (Exception $e) {
    413             // Hack... detect exception type. We really should use exception subclasses.
    414             // if the exception isn't a 'no method' error, rethrow it
    415             if ($e->getCode() !== 2175) throw $e;
    416             $original = $this->copyContentFrom();
    417             $originalClass = get_class($original);
    418             if ($originalClass == 'SiteTree') $name = 'ContentController';
    419             else $name = $originalClass."_Controller";
    420             $controller = new $name($this->dataRecord->copyContentFrom());
    421             return call_user_func_array(array($controller, $method), $args);
    422         }
    423     }
    424 }
    425
    426 ?>
    427

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -