Completed
Pull Request — master (#1833)
by
unknown
02:39
created

VirtualPage::allMethodNames()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 10
rs 9.4285
cc 2
eloc 5
nc 2
nop 1
1
<?php
2
3
namespace SilverStripe\CMS\Model;
4
5
use SilverStripe\Core\Convert;
6
use SilverStripe\Forms\FieldList;
7
use SilverStripe\Forms\LiteralField;
8
use SilverStripe\Forms\ReadonlyTransformation;
9
use SilverStripe\Forms\TreeDropdownField;
10
use SilverStripe\ORM\DataObject;
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
19
 * duplicating the content.
20
 *
21
 * Note: This Only duplicates $db fields and not the $has_one etc..
22
 *
23
 * @method SiteTree CopyContentFrom()
24
 * @property int $CopyContentFromID
25
 */
26
class VirtualPage extends Page
27
{
28
29
    private static $description = 'Displays the content of another page';
30
31
    public static $virtualFields;
32
33
    /**
34
     * @var array Define fields that are not virtual - the virtual page must define these fields themselves.
35
     * Note that anything in {@link self::config()->initially_copied_fields} is implicitly included in this list.
36
     */
37
    private static $non_virtual_fields = array(
38
        "ID",
39
        "ClassName",
40
        "ObsoleteClassName",
41
        "SecurityTypeID",
42
        "OwnerID",
43
        "ParentID",
44
        "URLSegment",
45
        "Sort",
46
        "Status",
47
        'ShowInMenus',
48
        // 'Locale'
49
        'ShowInSearch',
50
        'Version',
51
        "Embargo",
52
        "Expiry",
53
        "CanViewType",
54
        "CanEditType",
55
        "CopyContentFromID",
56
        "HasBrokenLink",
57
    );
58
59
    /**
60
     * @var array Define fields that are initially copied to virtual pages but left modifiable after that.
61
     */
62
    private static $initially_copied_fields = array(
63
        'ShowInMenus',
64
        'ShowInSearch',
65
        'URLSegment',
66
    );
67
68
    private static $has_one = array(
69
        "CopyContentFrom" => "SilverStripe\\CMS\\Model\\SiteTree",
70
    );
71
72
    private static $owns = array(
73
        "CopyContentFrom",
74
    );
75
76
    private static $db = array(
77
        "VersionID" => "Int",
78
    );
79
80
    private static $table_name = 'VirtualPage';
81
82
    /**
83
     * Generates the array of fields required for the page type.
84
     *
85
     * @return array
86
     */
87
    public function getVirtualFields()
88
    {
89
        // Check if copied page exists
90
        $record = $this->CopyContentFrom();
91
        if (!$record || !$record->exists()) {
92
            return array();
93
        }
94
95
        // Diff db with non-virtual fields
96
        $fields = array_keys(static::getSchema()->fieldSpecs($record));
97
        $nonVirtualFields = $this->getNonVirtualisedFields();
98
        return array_diff($fields, $nonVirtualFields);
99
    }
100
101
    /**
102
     * List of fields or properties to never virtualise
103
     *
104
     * @return array
105
     */
106
    public function getNonVirtualisedFields()
107
    {
108
        return array_merge(
109
            self::config()->non_virtual_fields,
110
            self::config()->initially_copied_fields
111
        );
112
    }
113
114
    public function setCopyContentFromID($val)
115
    {
116
        // Sanity check to prevent pages virtualising other virtual pages
117
        if ($val && DataObject::get_by_id('SilverStripe\\CMS\\Model\\SiteTree', $val) instanceof VirtualPage) {
118
            $val = 0;
119
        }
120
        return $this->setField("CopyContentFromID", $val);
121
    }
122
123
    public function ContentSource()
124
    {
125
        $copied = $this->CopyContentFrom();
126
        if ($copied && $copied->exists()) {
127
            return $copied;
128
        }
129
        return $this;
130
    }
131
132
    /**
133
     * For VirtualPage, add a canonical link tag linking to the original page
134
     * See TRAC #6828 & http://support.google.com/webmasters/bin/answer.py?hl=en&answer=139394
135
     *
136
     * @param boolean $includeTitle Show default <title>-tag, set to false for custom templating
137
     * @return string The XHTML metatags
138
     */
139
    public function MetaTags($includeTitle = true)
140
    {
141
        $tags = parent::MetaTags($includeTitle);
142
        $copied = $this->CopyContentFrom();
143
        if ($copied && $copied->exists()) {
144
            $link = Convert::raw2att($copied->Link());
145
            $tags .= "<link rel=\"canonical\" href=\"{$link}\" />\n";
146
        }
147
        return $tags;
148
    }
149
150
    public function allowedChildren()
151
    {
152
        $copy = $this->CopyContentFrom();
153
        if ($copy && $copy->exists()) {
154
            return $copy->allowedChildren();
155
        }
156
        return array();
157
    }
158
159
    public function syncLinkTracking()
160
    {
161
        if ($this->CopyContentFromID) {
162
            $copyPage = DataObject::get_by_id('SilverStripe\\CMS\\Model\\SiteTree', $this->CopyContentFromID);
163
            $this->HasBrokenLink = !$copyPage;
164
        } else {
165
            $this->HasBrokenLink = true;
166
        }
167
    }
168
169
    /**
170
     * We can only publish the page if there is a published source page
171
     *
172
     * @param Member $member Member to check
173
     * @return bool
174
     */
175
    public function canPublish($member = null)
176
    {
177
        return $this->isPublishable() && parent::canPublish($member);
178
    }
179
180
    /**
181
     * Returns true if is page is publishable by anyone at all
182
     * Return false if the source page isn't published yet.
183
     *
184
     * Note that isPublishable doesn't affect ete from live, only publish.
185
     */
186
    public function isPublishable()
187
    {
188
        // No source
189
        if (!$this->CopyContentFrom() || !$this->CopyContentFrom()->ID) {
190
            return false;
191
        }
192
193
        // Unpublished source
194
        if (!Versioned::get_versionnumber_by_stage(
195
            'SilverStripe\\CMS\\Model\\SiteTree',
196
            'Live',
197
            $this->CopyContentFromID
198
        )) {
199
            return false;
200
        }
201
202
        // Default - publishable
203
        return true;
204
    }
205
206
    /**
207
     * Generate the CMS fields from the fields from the original page.
208
     */
209
    public function getCMSFields()
210
    {
211
        $this->beforeUpdateCMSFields(function (FieldList $fields) {
212
            // Setup the linking to the original page.
213
            $copyContentFromField = TreeDropdownField::create(
214
                'CopyContentFromID',
215
                _t('SilverStripe\\CMS\\Model\\VirtualPage.CHOOSE', "Linked Page"),
216
                "SilverStripe\\CMS\\Model\\SiteTree"
217
            );
218
219
            // Setup virtual fields
220
            if ($virtualFields = $this->getVirtualFields()) {
221
                $roTransformation = new ReadonlyTransformation();
222
                foreach ($virtualFields as $virtualField) {
223
                    if ($fields->dataFieldByName($virtualField)) {
224
                        $fields->replaceField(
225
                            $virtualField,
226
                            $fields->dataFieldByName($virtualField)->transform($roTransformation)
227
                        );
228
                    }
229
                }
230
            }
231
232
            $msgs = array();
233
234
            $fields->addFieldToTab('Root.Main', $copyContentFromField, 'Title');
235
236
            // Create links back to the original object in the CMS
237
            if ($this->CopyContentFrom()->exists()) {
238
                $link = "<a class=\"cmsEditlink\" href=\"admin/pages/edit/show/$this->CopyContentFromID\">" . _t(
239
                    'SilverStripe\\CMS\\Model\\VirtualPage.EditLink',
240
                    'edit'
241
                ) . "</a>";
242
                $msgs[] = _t(
243
                    'SilverStripe\\CMS\\Model\\VirtualPage.HEADERWITHLINK',
244
                    "This is a virtual page copying content from \"{title}\" ({link})",
245
                    array(
246
                        'title' => $this->CopyContentFrom()->obj('Title'),
247
                        'link'  => $link,
248
                    )
249
                );
250
            } else {
251
                $msgs[] = _t('SilverStripe\\CMS\\Model\\VirtualPage.HEADER', "This is a virtual page");
252
                $msgs[] = _t(
253
                    'SilverStripe\\CMS\\Model\\SiteTree.VIRTUALPAGEWARNING',
254
                    'Please choose a linked page and save first in order to publish this page'
255
                );
256
            }
257
            if ($this->CopyContentFromID && !Versioned::get_versionnumber_by_stage(
258
                SiteTree::class,
259
                Versioned::LIVE,
260
                $this->CopyContentFromID
261
            )
262
            ) {
263
                $msgs[] = _t(
264
                    'SilverStripe\\CMS\\Model\\SiteTree.VIRTUALPAGEDRAFTWARNING',
265
                    'Please publish the linked page in order to publish the virtual page'
266
                );
267
            }
268
269
            $fields->addFieldToTab("Root.Main", new LiteralField(
270
                'VirtualPageMessage',
271
                '<div class="message notice">' . implode('. ', $msgs) . '.</div>'
272
            ), 'CopyContentFromID');
273
        });
274
275
        return parent::getCMSFields();
276
    }
277
278
    public function onBeforeWrite()
279
    {
280
        parent::onBeforeWrite();
281
        $this->refreshFromCopied();
282
    }
283
284
    /**
285
     * Copy any fields from the copied record to bootstrap /backup
286
     */
287
    protected function refreshFromCopied()
288
    {
289
        // Skip if copied record isn't available
290
        $source = $this->CopyContentFrom();
291
        if (!$source || !$source->exists()) {
292
            return;
293
        }
294
295
        // We also want to copy certain, but only if we're copying the source page for the first
296
        // time. After this point, the user is free to customise these for the virtual page themselves.
297
        if ($this->isChanged('CopyContentFromID', 2) && $this->CopyContentFromID) {
298
            foreach (self::config()->initially_copied_fields as $fieldName) {
299
                $this->$fieldName = $source->$fieldName;
300
            }
301
        }
302
303
        // Copy fields to the original record in case the class type changes
304
        foreach ($this->getVirtualFields() as $virtualField) {
305
            $this->$virtualField = $source->$virtualField;
306
        }
307
    }
308
309
    public function getSettingsFields()
310
    {
311
        $fields = parent::getSettingsFields();
312
        if (!$this->CopyContentFrom()->exists()) {
313
            $fields->addFieldToTab(
314
                "Root.Settings",
315
                new LiteralField(
316
                    'VirtualPageWarning',
317
                    '<div class="message notice">'
318
                     . _t(
319
                         'SilverStripe\\CMS\\Model\\SiteTree.VIRTUALPAGEWARNINGSETTINGS',
320
                         'Please choose a linked page in the main content fields in order to publish'
321
                     )
322
                    . '</div>'
323
                ),
324
                'ClassName'
325
            );
326
        }
327
328
        return $fields;
329
    }
330
331
    public function validate()
332
    {
333
        $result = parent::validate();
334
335
        // "Can be root" validation
336
        $orig = $this->CopyContentFrom();
337
        if ($orig && $orig->exists() && !$orig->stat('can_be_root') && !$this->ParentID) {
338
            $result->addError(
339
                _t(
340
                    'SilverStripe\\CMS\\Model\\VirtualPage.PageTypNotAllowedOnRoot',
341
                    'Original page type "{type}" is not allowed on the root level for this virtual page',
342
                    array('type' => $orig->i18n_singular_name())
343
                ),
344
                ValidationResult::TYPE_ERROR,
345
                'CAN_BE_ROOT_VIRTUAL'
346
            );
347
        }
348
349
        return $result;
350
    }
351
352
    public function updateImageTracking()
353
    {
354
        // Doesn't work on unsaved records
355
        if (!$this->ID) {
356
            return;
357
        }
358
359
        // Remove CopyContentFrom() from the cache
360
        unset($this->components['CopyContentFrom']);
361
362
        // Update ImageTracking
363
        $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...
364
    }
365
366
    public function CMSTreeClasses()
367
    {
368
        return parent::CMSTreeClasses() . ' VirtualPage-' . $this->CopyContentFrom()->ClassName;
369
    }
370
371
    /**
372
     * Allow attributes on the master page to pass
373
     * through to the virtual page
374
     *
375
     * @param string $field
376
     * @return mixed
377
     */
378
    public function __get($field)
379
    {
380
        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...
381
            return $this->$funcName();
382
        }
383
        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...
384
            return $this->getField($field);
385
        }
386
        if (($copy = $this->CopyContentFrom()) && $copy->exists()) {
387
            return $copy->$field;
388
        }
389
        return null;
390
    }
391
392
    public function getField($field)
393
    {
394
        if ($this->isFieldVirtualised($field)) {
395
            return $this->CopyContentFrom()->getField($field);
396
        }
397
        return parent::getField($field);
398
    }
399
400
    /**
401
     * Check if given field is virtualised
402
     *
403
     * @param string $field
404
     * @return bool
405
     */
406
    public function isFieldVirtualised($field)
407
    {
408
        // Don't defer if field is non-virtualised
409
        $ignore = $this->getNonVirtualisedFields();
410
        if (in_array($field, $ignore)) {
411
            return false;
412
        }
413
414
        // Don't defer if no virtual page
415
        $copied = $this->CopyContentFrom();
416
        if (!$copied || !$copied->exists()) {
417
            return false;
418
        }
419
420
        // Check if copied object has this field
421
        return $copied->hasField($field);
422
    }
423
424
    /**
425
     * Pass unrecognized method calls on to the original data object
426
     *
427
     * @param string $method
428
     * @param array $args
429
     * @return mixed
430
     */
431
    public function __call($method, $args)
432
    {
433
        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...
434
            return parent::__call($method, $args);
435
        } else {
436
            return call_user_func_array(array($this->CopyContentFrom(), $method), $args);
437
        }
438
    }
439
440
    /**
441
     * @param string $field
442
     * @return bool
443
     */
444
    public function hasField($field)
445
    {
446
        if (parent::hasField($field)) {
447
            return true;
448
        }
449
        $copy = $this->CopyContentFrom();
450
        return $copy && $copy->exists() && $copy->hasField($field);
451
    }
452
453
    /**
454
     * Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value object) for a field
455
     * on this object.
456
     *
457
     * @param string $field
458
     * @return string
459
     */
460
    public function castingHelper($field)
461
    {
462
        $copy = $this->CopyContentFrom();
463
        if ($copy && $copy->exists() && ($helper = $copy->castingHelper($field))) {
464
            return $helper;
465
        }
466
        return parent::castingHelper($field);
467
    }
468
469
    /**
470
     * {@inheritdoc}
471
     */
472
    public function allMethodNames($custom = false)
473
    {
474
        $methods = parent::allMethodNames($custom);
475
476
        if ($copy = $this->CopyContentFrom()) {
477
            $methods = array_merge($methods, $copy->allMethodNames($custom));
478
        }
479
480
        return $methods;
481
    }
482
483
    /**
484
     * {@inheritdoc}
485
     */
486
    public function getControllerName()
487
    {
488
        if ($copy = $this->CopyContentFrom()) {
489
            return $copy->getControllerName();
490
        }
491
492
        return parent::getControllerName();
493
    }
494
}
495