Completed
Pull Request — master (#1827)
by Franco
02:37
created

VirtualPage::isFieldVirtualised()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 17
rs 9.2
cc 4
eloc 8
nc 3
nop 1
1
<?php
2
3
namespace SilverStripe\CMS\Model;
4
5
use SilverStripe\Core\Convert;
6
use SilverStripe\Forms\LiteralField;
7
use SilverStripe\Forms\ReadonlyTransformation;
8
use SilverStripe\Forms\TreeDropdownField;
9
use SilverStripe\ORM\DataObject;
10
use SilverStripe\ORM\Hierarchy\MarkedSet;
11
use SilverStripe\ORM\ValidationResult;
12
use SilverStripe\Versioned\Versioned;
13
use SilverStripe\Security\Member;
14
use Page;
15
16
/**
17
 * Virtual Page creates an instance of a  page, with the same fields that the original page had, but readonly.
18
 * This allows you can have a page in mulitple places in the site structure, with different children without duplicating the content
19
 * Note: This Only duplicates $db fields and not the $has_one etc..
20
 *
21
 * @method SiteTree CopyContentFrom()
22
 * @property int $CopyContentFromID
23
 */
24
class VirtualPage extends Page
25
{
26
27
    private static $description = 'Displays the content of another page';
28
29
    public static $virtualFields;
30
31
    /**
32
     * @var array Define fields that are not virtual - the virtual page must define these fields themselves.
33
     * Note that anything in {@link self::config()->initially_copied_fields} is implicitly included in this list.
34
     */
35
    private static $non_virtual_fields = array(
36
        "ID",
37
        "ClassName",
38
        "ObsoleteClassName",
39
        "SecurityTypeID",
40
        "OwnerID",
41
        "ParentID",
42
        "URLSegment",
43
        "Sort",
44
        "Status",
45
        'ShowInMenus',
46
        // 'Locale'
47
        'ShowInSearch',
48
        'Version',
49
        "Embargo",
50
        "Expiry",
51
        "CanViewType",
52
        "CanEditType",
53
        "CopyContentFromID",
54
        "HasBrokenLink",
55
    );
56
57
    /**
58
     * @var array Define fields that are initially copied to virtual pages but left modifiable after that.
59
     */
60
    private static $initially_copied_fields = array(
61
        'ShowInMenus',
62
        'ShowInSearch',
63
        'URLSegment',
64
    );
65
66
    private static $has_one = array(
67
        "CopyContentFrom" => "SilverStripe\\CMS\\Model\\SiteTree",
68
    );
69
70
    private static $owns = array(
71
        "CopyContentFrom",
72
    );
73
74
    private static $db = array(
75
        "VersionID" => "Int",
76
    );
77
78
    private static $table_name = 'VirtualPage';
79
80
    /**
81
     * Generates the array of fields required for the page type.
82
     *
83
     * @return array
84
     */
85
    public function getVirtualFields()
86
    {
87
        // Check if copied page exists
88
        $record = $this->CopyContentFrom();
89
        if (!$record || !$record->exists()) {
90
            return array();
91
        }
92
93
        // Diff db with non-virtual fields
94
        $fields = array_keys(static::getSchema()->fieldSpecs($record));
95
        $nonVirtualFields = $this->getNonVirtualisedFields();
96
        return array_diff($fields, $nonVirtualFields);
97
    }
98
99
    /**
100
     * List of fields or properties to never virtualise
101
     *
102
     * @return array
103
     */
104
    public function getNonVirtualisedFields()
105
    {
106
        return array_merge(
107
            self::config()->non_virtual_fields,
108
            self::config()->initially_copied_fields
109
        );
110
    }
111
112
    public function setCopyContentFromID($val)
113
    {
114
        // Sanity check to prevent pages virtualising other virtual pages
115
        if ($val && DataObject::get_by_id('SilverStripe\\CMS\\Model\\SiteTree', $val) instanceof VirtualPage) {
116
            $val = 0;
117
        }
118
        return $this->setField("CopyContentFromID", $val);
119
    }
120
121
    public function ContentSource()
122
    {
123
        $copied = $this->CopyContentFrom();
124
        if ($copied && $copied->exists()) {
125
            return $copied;
126
        }
127
        return $this;
128
    }
129
130
    /**
131
     * For VirtualPage, add a canonical link tag linking to the original page
132
     * See TRAC #6828 & http://support.google.com/webmasters/bin/answer.py?hl=en&answer=139394
133
     *
134
     * @param boolean $includeTitle Show default <title>-tag, set to false for custom templating
135
     * @return string The XHTML metatags
136
     */
137
    public function MetaTags($includeTitle = true)
138
    {
139
        $tags = parent::MetaTags($includeTitle);
140
        $copied = $this->CopyContentFrom();
141
        if ($copied && $copied->exists()) {
142
            $link = Convert::raw2att($copied->Link());
143
            $tags .= "<link rel=\"canonical\" href=\"{$link}\" />\n";
144
        }
145
        return $tags;
146
    }
147
148
    public function allowedChildren()
149
    {
150
        $copy = $this->CopyContentFrom();
151
        if ($copy && $copy->exists()) {
152
            return $copy->allowedChildren();
153
        }
154
        return array();
155
    }
156
157
    public function syncLinkTracking()
158
    {
159
        if ($this->CopyContentFromID) {
160
            $this->HasBrokenLink = !(bool) DataObject::get_by_id('SilverStripe\\CMS\\Model\\SiteTree', $this->CopyContentFromID);
161
        } else {
162
            $this->HasBrokenLink = true;
163
        }
164
    }
165
166
    /**
167
     * We can only publish the page if there is a published source page
168
     *
169
     * @param Member $member Member to check
170
     * @return bool
171
     */
172
    public function canPublish($member = null)
173
    {
174
        return $this->isPublishable() && parent::canPublish($member);
175
    }
176
177
    /**
178
     * Returns true if is page is publishable by anyone at all
179
     * Return false if the source page isn't published yet.
180
     *
181
     * Note that isPublishable doesn't affect ete from live, only publish.
182
     */
183
    public function isPublishable()
184
    {
185
        // No source
186
        if (!$this->CopyContentFrom() || !$this->CopyContentFrom()->ID) {
187
            return false;
188
        }
189
190
        // Unpublished source
191
        if (!Versioned::get_versionnumber_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 'Live', $this->CopyContentFromID)) {
192
            return false;
193
        }
194
195
        // Default - publishable
196
        return true;
197
    }
198
199
    /**
200
     * Generate the CMS fields from the fields from the original page.
201
     */
202
    public function getCMSFields()
203
    {
204
        $this->beforeUpdateCMSFields(function (FieldList $fields) {
205
            // Setup the linking to the original page.
206
            $copyContentFromField = TreeDropdownField::create(
207
                'CopyContentFromID',
208
                _t('SilverStripe\\CMS\\Model\\VirtualPage.CHOOSE', "Linked Page"),
209
                "SilverStripe\\CMS\\Model\\SiteTree"
210
            );
211
            // filter doesn't let you select children of virtual pages as as source page
212
            //$copyContentFromField->setFilterFunction(create_function('$item', 'return !($item instanceof VirtualPage);'));
213
214
            // Setup virtual fields
215
            if ($virtualFields = $this->getVirtualFields()) {
216
                $roTransformation = new ReadonlyTransformation();
217
                foreach ($virtualFields as $virtualField) {
218
                    if ($fields->dataFieldByName($virtualField)) {
219
                        $fields->replaceField(
220
                            $virtualField,
221
                            $fields->dataFieldByName($virtualField)->transform($roTransformation)
222
                        );
223
                    }
224
                }
225
            }
226
227
            $msgs = array();
228
229
            $fields->addFieldToTab('Root.Main', $copyContentFromField, 'Title');
230
231
            // Create links back to the original object in the CMS
232
            if ($this->CopyContentFrom()->exists()) {
233
                $link = "<a class=\"cmsEditlink\" href=\"admin/pages/edit/show/$this->CopyContentFromID\">" . _t(
234
                    'SilverStripe\\CMS\\Model\\VirtualPage.EditLink',
235
                    'edit'
236
                ) . "</a>";
237
                $msgs[] = _t(
238
                    'SilverStripe\\CMS\\Model\\VirtualPage.HEADERWITHLINK',
239
                    "This is a virtual page copying content from \"{title}\" ({link})",
240
                    array(
241
                        'title' => $this->CopyContentFrom()->obj('Title'),
242
                        'link'  => $link,
243
                    )
244
                );
245
            } else {
246
                $msgs[] = _t('SilverStripe\\CMS\\Model\\VirtualPage.HEADER', "This is a virtual page");
247
                $msgs[] = _t(
248
                    'SilverStripe\\CMS\\Model\\SiteTree.VIRTUALPAGEWARNING',
249
                    'Please choose a linked page and save first in order to publish this page'
250
                );
251
            }
252
            if ($this->CopyContentFromID && !Versioned::get_versionnumber_by_stage(
253
                SiteTree::class,
254
                Versioned::LIVE,
255
                $this->CopyContentFromID
256
            )
257
            ) {
258
                $msgs[] = _t(
259
                    'SilverStripe\\CMS\\Model\\SiteTree.VIRTUALPAGEDRAFTWARNING',
260
                    'Please publish the linked page in order to publish the virtual page'
261
                );
262
            }
263
264
            $fields->addFieldToTab("Root.Main", new LiteralField(
265
                'VirtualPageMessage',
266
                '<div class="message notice">' . implode('. ', $msgs) . '.</div>'
267
            ), 'CopyContentFromID');
268
        });
269
270
        return parent::getCMSFields();
271
    }
272
273
    public function onBeforeWrite()
274
    {
275
        parent::onBeforeWrite();
276
        $this->refreshFromCopied();
277
    }
278
279
    /**
280
     * Copy any fields from the copied record to bootstrap /backup
281
     */
282
    protected function refreshFromCopied()
283
    {
284
        // Skip if copied record isn't available
285
        $source = $this->CopyContentFrom();
286
        if (!$source || !$source->exists()) {
287
            return;
288
        }
289
290
        // We also want to copy certain, but only if we're copying the source page for the first
291
        // time. After this point, the user is free to customise these for the virtual page themselves.
292
        if ($this->isChanged('CopyContentFromID', 2) && $this->CopyContentFromID) {
293
            foreach (self::config()->initially_copied_fields as $fieldName) {
294
                $this->$fieldName = $source->$fieldName;
295
            }
296
        }
297
298
        // Copy fields to the original record in case the class type changes
299
        foreach ($this->getVirtualFields() as $virtualField) {
300
            $this->$virtualField = $source->$virtualField;
301
        }
302
    }
303
304
    public function getSettingsFields()
305
    {
306
        $fields = parent::getSettingsFields();
307
        if (!$this->CopyContentFrom()->exists()) {
308
            $fields->addFieldToTab(
309
                "Root.Settings",
310
                new LiteralField(
311
                    'VirtualPageWarning',
312
                    '<div class="message notice">'
313
                     . _t(
314
                         'SilverStripe\\CMS\\Model\\SiteTree.VIRTUALPAGEWARNINGSETTINGS',
315
                         'Please choose a linked page in the main content fields in order to publish'
316
                     )
317
                    . '</div>'
318
                ),
319
                'ClassName'
320
            );
321
        }
322
323
        return $fields;
324
    }
325
326
    public function validate()
327
    {
328
        $result = parent::validate();
329
330
        // "Can be root" validation
331
        $orig = $this->CopyContentFrom();
332
        if ($orig && $orig->exists() && !$orig->stat('can_be_root') && !$this->ParentID) {
333
            $result->addError(
334
                _t(
335
                    'SilverStripe\\CMS\\Model\\VirtualPage.PageTypNotAllowedOnRoot',
336
                    'Original page type "{type}" is not allowed on the root level for this virtual page',
337
                    array('type' => $orig->i18n_singular_name())
338
                ),
339
                ValidationResult::TYPE_ERROR,
340
                'CAN_BE_ROOT_VIRTUAL'
341
            );
342
        }
343
344
        return $result;
345
    }
346
347
    public function updateImageTracking()
348
    {
349
        // Doesn't work on unsaved records
350
        if (!$this->ID) {
351
            return;
352
        }
353
354
        // Remove CopyContentFrom() from the cache
355
        unset($this->components['CopyContentFrom']);
356
357
        // Update ImageTracking
358
        $this->ImageTracking()->setByIDList($this->CopyContentFrom()->ImageTracking()->column('ID'));
0 ignored issues
show
Documentation Bug introduced by
The method ImageTracking does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
359
    }
360
361
    public function CMSTreeClasses()
362
    {
363
        return parent::CMSTreeClasses() . ' VirtualPage-' . $this->CopyContentFrom()->ClassName;
364
    }
365
366
    /**
367
     * Allow attributes on the master page to pass
368
     * through to the virtual page
369
     *
370
     * @param string $field
371
     * @return mixed
372
     */
373
    public function __get($field)
374
    {
375
        if (parent::hasMethod($funcName = "get$field")) {
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (hasMethod() instead of __get()). Are you sure this is correct? If so, you might want to change this to $this->hasMethod().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
376
            return $this->$funcName();
377
        }
378
        if (parent::hasField($field) || ($field === 'ID' && !$this->exists())) {
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (hasField() instead of __get()). Are you sure this is correct? If so, you might want to change this to $this->hasField().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
379
            return $this->getField($field);
380
        }
381
        if (($copy = $this->CopyContentFrom()) && $copy->exists()) {
382
            return $copy->$field;
383
        }
384
        return null;
385
    }
386
387
    public function getField($field)
388
    {
389
        if ($this->isFieldVirtualised($field)) {
390
            return $this->CopyContentFrom()->getField($field);
391
        }
392
        return parent::getField($field);
393
    }
394
395
    /**
396
     * Check if given field is virtualised
397
     *
398
     * @param string $field
399
     * @return bool
400
     */
401
    public function isFieldVirtualised($field)
402
    {
403
        // Don't defer if field is non-virtualised
404
        $ignore = $this->getNonVirtualisedFields();
405
        if (in_array($field, $ignore)) {
406
            return false;
407
        }
408
409
        // Don't defer if no virtual page
410
        $copied = $this->CopyContentFrom();
411
        if (!$copied || !$copied->exists()) {
412
            return false;
413
        }
414
415
        // Check if copied object has this field
416
        return $copied->hasField($field);
417
    }
418
419
    /**
420
     * Pass unrecognized method calls on to the original data object
421
     *
422
     * @param string $method
423
     * @param array $args
424
     * @return mixed
425
     */
426
    public function __call($method, $args)
427
    {
428
        if (parent::hasMethod($method)) {
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (hasMethod() instead of __call()). Are you sure this is correct? If so, you might want to change this to $this->hasMethod().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
429
            return parent::__call($method, $args);
430
        } else {
431
            return call_user_func_array(array($this->CopyContentFrom(), $method), $args);
432
        }
433
    }
434
435
    /**
436
     * @param string $field
437
     * @return bool
438
     */
439
    public function hasField($field)
440
    {
441
        if (parent::hasField($field)) {
442
            return true;
443
        }
444
        $copy = $this->CopyContentFrom();
445
        return $copy && $copy->exists() && $copy->hasField($field);
446
    }
447
448
    /**
449
     * Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value object) for a field
450
     * on this object.
451
     *
452
     * @param string $field
453
     * @return string
454
     */
455
    public function castingHelper($field)
456
    {
457
        $copy = $this->CopyContentFrom();
458
        if ($copy && $copy->exists() && ($helper = $copy->castingHelper($field))) {
459
            return $helper;
460
        }
461
        return parent::castingHelper($field);
462
    }
463
464
    /**
465
     * {@inheritdoc}
466
     */
467
    public function allMethodNames($custom = false)
468
    {
469
        $methods = parent::allMethodNames($custom);
470
471
        if ($copy = $this->CopyContentFrom()) {
472
            $methods = array_merge($methods, $copy->allMethodNames($custom));
473
        }
474
475
        return $methods;
476
    }
477
478
    /**
479
     * {@inheritdoc}
480
     */
481
    public function getControllerName()
482
    {
483
        if ($copy = $this->CopyContentFrom()) {
484
            return $copy->getControllerName();
485
        }
486
487
        return parent::getControllerName();
488
    }
489
}
490