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

VirtualPage::validate()   B

Complexity

Conditions 5
Paths 2

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 12
nc 2
nop 0
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
274
    public function onBeforeWrite()
275
    {
276
        parent::onBeforeWrite();
277
        $this->refreshFromCopied();
278
    }
279
280
    /**
281
     * Copy any fields from the copied record to bootstrap /backup
282
     */
283
    protected function refreshFromCopied()
284
    {
285
        // Skip if copied record isn't available
286
        $source = $this->CopyContentFrom();
287
        if (!$source || !$source->exists()) {
288
            return;
289
        }
290
291
        // We also want to copy certain, but only if we're copying the source page for the first
292
        // time. After this point, the user is free to customise these for the virtual page themselves.
293
        if ($this->isChanged('CopyContentFromID', 2) && $this->CopyContentFromID) {
294
            foreach (self::config()->initially_copied_fields as $fieldName) {
295
                $this->$fieldName = $source->$fieldName;
296
            }
297
        }
298
299
        // Copy fields to the original record in case the class type changes
300
        foreach ($this->getVirtualFields() as $virtualField) {
301
            $this->$virtualField = $source->$virtualField;
302
        }
303
    }
304
305
    public function getSettingsFields()
306
    {
307
        $fields = parent::getSettingsFields();
308
        if (!$this->CopyContentFrom()->exists()) {
309
            $fields->addFieldToTab(
310
                "Root.Settings",
311
                new LiteralField(
312
                    'VirtualPageWarning',
313
                    '<div class="message notice">'
314
                     . _t(
315
                         'SilverStripe\\CMS\\Model\\SiteTree.VIRTUALPAGEWARNINGSETTINGS',
316
                         'Please choose a linked page in the main content fields in order to publish'
317
                     )
318
                    . '</div>'
319
                ),
320
                'ClassName'
321
            );
322
        }
323
324
        return $fields;
325
    }
326
327
    public function validate()
328
    {
329
        $result = parent::validate();
330
331
        // "Can be root" validation
332
        $orig = $this->CopyContentFrom();
333
        if ($orig && $orig->exists() && !$orig->stat('can_be_root') && !$this->ParentID) {
334
            $result->addError(
335
                _t(
336
                    'SilverStripe\\CMS\\Model\\VirtualPage.PageTypNotAllowedOnRoot',
337
                    'Original page type "{type}" is not allowed on the root level for this virtual page',
338
                    array('type' => $orig->i18n_singular_name())
339
                ),
340
                ValidationResult::TYPE_ERROR,
341
                'CAN_BE_ROOT_VIRTUAL'
342
            );
343
        }
344
345
        return $result;
346
    }
347
348
    public function updateImageTracking()
349
    {
350
        // Doesn't work on unsaved records
351
        if (!$this->ID) {
352
            return;
353
        }
354
355
        // Remove CopyContentFrom() from the cache
356
        unset($this->components['CopyContentFrom']);
357
358
        // Update ImageTracking
359
        $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...
360
    }
361
362
    public function CMSTreeClasses()
363
    {
364
        return parent::CMSTreeClasses() . ' VirtualPage-' . $this->CopyContentFrom()->ClassName;
365
    }
366
367
    /**
368
     * Allow attributes on the master page to pass
369
     * through to the virtual page
370
     *
371
     * @param string $field
372
     * @return mixed
373
     */
374
    public function __get($field)
375
    {
376
        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...
377
            return $this->$funcName();
378
        }
379
        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...
380
            return $this->getField($field);
381
        }
382
        if (($copy = $this->CopyContentFrom()) && $copy->exists()) {
383
            return $copy->$field;
384
        }
385
        return null;
386
    }
387
388
    public function getField($field)
389
    {
390
        if ($this->isFieldVirtualised($field)) {
391
            return $this->CopyContentFrom()->getField($field);
392
        }
393
        return parent::getField($field);
394
    }
395
396
    /**
397
     * Check if given field is virtualised
398
     *
399
     * @param string $field
400
     * @return bool
401
     */
402
    public function isFieldVirtualised($field)
403
    {
404
        // Don't defer if field is non-virtualised
405
        $ignore = $this->getNonVirtualisedFields();
406
        if (in_array($field, $ignore)) {
407
            return false;
408
        }
409
410
        // Don't defer if no virtual page
411
        $copied = $this->CopyContentFrom();
412
        if (!$copied || !$copied->exists()) {
413
            return false;
414
        }
415
416
        // Check if copied object has this field
417
        return $copied->hasField($field);
418
    }
419
420
    /**
421
     * Pass unrecognized method calls on to the original data object
422
     *
423
     * @param string $method
424
     * @param array $args
425
     * @return mixed
426
     */
427
    public function __call($method, $args)
428
    {
429
        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...
430
            return parent::__call($method, $args);
431
        } else {
432
            return call_user_func_array(array($this->CopyContentFrom(), $method), $args);
433
        }
434
    }
435
436
    /**
437
     * @param string $field
438
     * @return bool
439
     */
440
    public function hasField($field)
441
    {
442
        if (parent::hasField($field)) {
443
            return true;
444
        }
445
        $copy = $this->CopyContentFrom();
446
        return $copy && $copy->exists() && $copy->hasField($field);
447
    }
448
449
    /**
450
     * Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value object) for a field
451
     * on this object.
452
     *
453
     * @param string $field
454
     * @return string
455
     */
456
    public function castingHelper($field)
457
    {
458
        $copy = $this->CopyContentFrom();
459
        if ($copy && $copy->exists() && ($helper = $copy->castingHelper($field))) {
460
            return $helper;
461
        }
462
        return parent::castingHelper($field);
463
    }
464
465
    /**
466
     * {@inheritdoc}
467
     */
468
    public function allMethodNames($custom = false)
469
    {
470
        $methods = parent::allMethodNames($custom);
471
472
        if ($copy = $this->CopyContentFrom()) {
473
            $methods = array_merge($methods, $copy->allMethodNames($custom));
474
        }
475
476
        return $methods;
477
    }
478
479
    /**
480
     * {@inheritdoc}
481
     */
482
    public function getControllerName()
483
    {
484
        if ($copy = $this->CopyContentFrom()) {
485
            return $copy->getControllerName();
486
        }
487
488
        return parent::getControllerName();
489
    }
490
}
491