Completed
Push — master ( 25a8fb...29fc7a )
by Hamish
12s
created

VirtualPage::setCopyContentFromID()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 7
rs 9.4285
c 1
b 0
f 0
cc 3
eloc 4
nc 2
nop 1
1
<?php
2
3
namespace SilverStripe\CMS\Model;
4
5
use Exception;
6
use SilverStripe\CMS\Controllers\ContentController;
7
use SilverStripe\ORM\DataObject;
8
use SilverStripe\ORM\Versioning\Versioned;
9
use Page;
10
use Convert;
11
use SilverStripe\Security\Member;
12
use TreeDropdownField;
13
use ReadonlyTransformation;
14
use LiteralField;
15
use Page_Controller;
16
use SilverStripe\CMS\Controllers\ModelAsController;
17
18
19
/**
20
* Virtual Page creates an instance of a  page, with the same fields that the original page had, but readonly.
21
* This allows you can have a page in mulitple places in the site structure, with different children without duplicating the content
22
* Note: This Only duplicates $db fields and not the $has_one etc..
23
 *
24
 * @method SiteTree CopyContentFrom()
25
 * @property int $CopyContentFromID
26
 *
27
* @package cms
28
*/
29
class VirtualPage extends Page {
30
31
	private static $description = 'Displays the content of another page';
32
33
	public static $virtualFields;
34
35
	/**
36
	 * @var array Define fields that are not virtual - the virtual page must define these fields themselves.
37
	 * Note that anything in {@link self::config()->initially_copied_fields} is implicitly included in this list.
38
	 */
39
	private static $non_virtual_fields = array(
40
		"ID",
41
		"ClassName",
42
		"ObsoleteClassName",
43
		"SecurityTypeID",
44
		"OwnerID",
45
		"ParentID",
46
		"URLSegment",
47
		"Sort",
48
		"Status",
49
		'ShowInMenus',
50
		// 'Locale'
51
		'ShowInSearch',
52
		'Version',
53
		"Embargo",
54
		"Expiry",
55
		"CanViewType",
56
		"CanEditType",
57
		"CopyContentFromID",
58
		"HasBrokenLink",
59
	);
60
61
	/**
62
	 * @var array Define fields that are initially copied to virtual pages but left modifiable after that.
63
	 */
64
	private static $initially_copied_fields = array(
65
		'ShowInMenus',
66
		'ShowInSearch',
67
		'URLSegment',
68
	);
69
70
	private static $has_one = array(
71
		"CopyContentFrom" => "SilverStripe\\CMS\\Model\\SiteTree",
72
	);
73
74
	private static $owns = array(
75
		"CopyContentFrom",
76
	);
77
78
	private static $db = array(
79
		"VersionID" => "Int",
80
	);
81
82
	private static $table_name = 'VirtualPage';
83
84
	/**
85
	 * Generates the array of fields required for the page type.
86
	 *
87
	 * @return array
88
	 */
89
	public function getVirtualFields() {
90
		// Check if copied page exists
91
		$record = $this->CopyContentFrom();
92
		if(!$record || !$record->exists()) {
93
			return array();
94
		}
95
96
		// Diff db with non-virtual fields
97
		$fields = array_keys($record->db());
98
		$nonVirtualFields = $this->getNonVirtualisedFields();
99
		return array_diff($fields, $nonVirtualFields);
100
	}
101
102
	/**
103
	 * List of fields or properties to never virtualise
104
	 *
105
	 * @return array
106
	 */
107
	public function getNonVirtualisedFields() {
108
		return array_merge($this->config()->non_virtual_fields, $this->config()->initially_copied_fields);
109
	}
110
111
	public function setCopyContentFromID($val) {
112
		// Sanity check to prevent pages virtualising other virtual pages
113
		if($val && DataObject::get_by_id('SilverStripe\\CMS\\Model\\SiteTree', $val) instanceof VirtualPage) {
114
			$val = 0;
115
		}
116
		return $this->setField("CopyContentFromID", $val);
117
	}
118
119
	public function ContentSource() {
120
		$copied = $this->CopyContentFrom();
121
		if($copied && $copied->exists()) {
122
			return $copied;
123
		}
124
		return $this;
125
	}
126
127
	/**
128
	 * For VirtualPage, add a canonical link tag linking to the original page
129
	 * See TRAC #6828 & http://support.google.com/webmasters/bin/answer.py?hl=en&answer=139394
130
	 *
131
	 * @param boolean $includeTitle Show default <title>-tag, set to false for custom templating
132
	 * @return string The XHTML metatags
133
	 */
134
	public function MetaTags($includeTitle = true) {
135
		$tags = parent::MetaTags($includeTitle);
136
		$copied = $this->CopyContentFrom();
137
		if ($copied && $copied->exists()) {
138
			$link = Convert::raw2att($copied->Link());
139
			$tags .= "<link rel=\"canonical\" href=\"{$link}\" />\n";
140
		}
141
		return $tags;
142
	}
143
144
	public function allowedChildren() {
145
		$copy = $this->CopyContentFrom();
146
		if($copy && $copy->exists()) {
147
			return $copy->allowedChildren();
148
		}
149
		return array();
150
	}
151
152
	public function syncLinkTracking() {
153
		if($this->CopyContentFromID) {
154
			$this->HasBrokenLink = !(bool) DataObject::get_by_id('SilverStripe\\CMS\\Model\\SiteTree', $this->CopyContentFromID);
155
		} else {
156
			$this->HasBrokenLink = true;
157
		}
158
	}
159
160
	/**
161
	 * We can only publish the page if there is a published source page
162
	 *
163
	 * @param Member $member Member to check
164
	 * @return bool
165
	 */
166
	public function canPublish($member = null) {
167
		return $this->isPublishable() && parent::canPublish($member);
168
	}
169
170
	/**
171
	 * Returns true if is page is publishable by anyone at all
172
	 * Return false if the source page isn't published yet.
173
	 *
174
	 * Note that isPublishable doesn't affect ete from live, only publish.
175
	 */
176
	public function isPublishable() {
177
		// No source
178
		if(!$this->CopyContentFrom() || !$this->CopyContentFrom()->ID) {
179
			return false;
180
		}
181
182
		// Unpublished source
183
		if(!Versioned::get_versionnumber_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 'Live', $this->CopyContentFromID)) {
184
			return false;
185
		}
186
187
		// Default - publishable
188
		return true;
189
	}
190
191
	/**
192
	 * Generate the CMS fields from the fields from the original page.
193
	 */
194
	public function getCMSFields() {
195
		$fields = parent::getCMSFields();
196
197
		// Setup the linking to the original page.
198
		$copyContentFromField = new TreeDropdownField(
199
			"CopyContentFromID",
200
			_t('VirtualPage.CHOOSE', "Linked Page"),
201
			"SilverStripe\\CMS\\Model\\SiteTree"
202
		);
203
		// filter doesn't let you select children of virtual pages as as source page
204
		//$copyContentFromField->setFilterFunction(create_function('$item', 'return !($item instanceof VirtualPage);'));
205
206
		// Setup virtual fields
207
		if($virtualFields = $this->getVirtualFields()) {
208
			$roTransformation = new ReadonlyTransformation();
209
			foreach($virtualFields as $virtualField) {
210
				if($fields->dataFieldByName($virtualField))
211
					$fields->replaceField($virtualField, $fields->dataFieldByName($virtualField)->transform($roTransformation));
212
			}
213
		}
214
215
		$msgs = array();
216
217
		$fields->addFieldToTab("Root.Main", $copyContentFromField, "Title");
218
219
		// Create links back to the original object in the CMS
220
		if($this->CopyContentFrom()->exists()) {
221
			$link = "<a class=\"cmsEditlink\" href=\"admin/pages/edit/show/$this->CopyContentFromID\">"
222
				. _t('VirtualPage.EditLink', 'edit')
223
				. "</a>";
224
			$msgs[] = _t(
225
				'VirtualPage.HEADERWITHLINK',
226
				"This is a virtual page copying content from \"{title}\" ({link})",
227
				array(
0 ignored issues
show
Documentation introduced by
array('title' => $this->...tle'), 'link' => $link) is of type array<string,object|stri...ject","link":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
228
					'title' => $this->CopyContentFrom()->obj('Title'),
229
					'link' => $link
230
				)
231
			);
232
		} else {
233
			$msgs[] = _t('VirtualPage.HEADER', "This is a virtual page");
234
			$msgs[] = _t(
235
				'SITETREE.VIRTUALPAGEWARNING',
236
				'Please choose a linked page and save first in order to publish this page'
237
			);
238
		}
239
		if(
240
			$this->CopyContentFromID
241
			&& !Versioned::get_versionnumber_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 'Live', $this->CopyContentFromID)
242
		) {
243
			$msgs[] = _t(
244
				'SITETREE.VIRTUALPAGEDRAFTWARNING',
245
				'Please publish the linked page in order to publish the virtual page'
246
			);
247
		}
248
249
		$fields->addFieldToTab("Root.Main",
250
			new LiteralField(
251
				'VirtualPageMessage',
252
				'<div class="message notice">' . implode('. ', $msgs) . '.</div>'
253
			),
254
			'CopyContentFromID'
255
		);
256
257
		return $fields;
258
	}
259
260
	public function onBeforeWrite() {
261
		parent::onBeforeWrite();
262
		$this->refreshFromCopied();
263
	}
264
265
	/**
266
	 * Copy any fields from the copied record to bootstrap /backup
267
	 */
268
	protected function refreshFromCopied() {
269
		// Skip if copied record isn't available
270
		$source = $this->CopyContentFrom();
271
		if(!$source || !$source->exists()) {
272
			return;
273
		}
274
275
		// We also want to copy certain, but only if we're copying the source page for the first
276
		// time. After this point, the user is free to customise these for the virtual page themselves.
277
		if($this->isChanged('CopyContentFromID', 2) && $this->CopyContentFromID) {
278
			foreach (self::config()->initially_copied_fields as $fieldName) {
279
				$this->$fieldName = $source->$fieldName;
280
			}
281
		}
282
283
		// Copy fields to the original record in case the class type changes
284
		foreach($this->getVirtualFields() as $virtualField) {
285
			$this->$virtualField = $source->$virtualField;
286
		}
287
	}
288
289
	public function getSettingsFields() {
290
		$fields = parent::getSettingsFields();
291
		if(!$this->CopyContentFrom()->exists()) {
292
			$fields->addFieldToTab("Root.Settings",
293
				new LiteralField(
294
					'VirtualPageWarning',
295
					'<div class="message notice">'
296
					 . _t(
297
							'SITETREE.VIRTUALPAGEWARNINGSETTINGS',
298
							'Please choose a linked page in the main content fields in order to publish'
299
						)
300
					. '</div>'
301
				),
302
				'ClassName'
303
			);
304
		}
305
306
		return $fields;
307
	}
308
309
	public function validate() {
310
		$result = parent::validate();
311
312
		// "Can be root" validation
313
		$orig = $this->CopyContentFrom();
314
		if($orig && $orig->exists() && !$orig->stat('can_be_root') && !$this->ParentID) {
315
			$result->error(
316
				_t(
317
					'VirtualPage.PageTypNotAllowedOnRoot',
318
					'Original page type "{type}" is not allowed on the root level for this virtual page',
319
					array('type' => $orig->i18n_singular_name())
0 ignored issues
show
Documentation introduced by
array('type' => $orig->i18n_singular_name()) is of type array<string,string,{"type":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
320
				),
321
				'CAN_BE_ROOT_VIRTUAL'
322
			);
323
		}
324
325
		return $result;
326
	}
327
328
	public function updateImageTracking() {
329
		// Doesn't work on unsaved records
330
		if(!$this->ID) return;
331
332
		// Remove CopyContentFrom() from the cache
333
		unset($this->components['CopyContentFrom']);
334
335
		// Update ImageTracking
336
		$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...
337
	}
338
339
	/**
340
	 * @param string $numChildrenMethod
341
	 * @return string
342
	 */
343
	public function CMSTreeClasses($numChildrenMethod="numChildren") {
344
		return parent::CMSTreeClasses($numChildrenMethod) . ' VirtualPage-' . $this->CopyContentFrom()->ClassName;
345
	}
346
347
	/**
348
	 * Allow attributes on the master page to pass
349
	 * through to the virtual page
350
	 *
351
	 * @param string $field
352
	 * @return mixed
353
	 */
354
	public function __get($field) {
355
		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...
356
			return $this->$funcName();
357
		}
358
		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...
359
			return $this->getField($field);
360
		}
361
		if(($copy = $this->CopyContentFrom()) && $copy->exists()) {
362
			return $copy->$field;
363
		}
364
		return null;
365
	}
366
367
	public function getField($field) {
368
		if($this->isFieldVirtualised($field)) {
369
			return $this->CopyContentFrom()->getField($field);
370
		}
371
		return parent::getField($field);
372
	}
373
374
	/**
375
	 * Check if given field is virtualised
376
	 *
377
	 * @param string $field
378
	 * @return bool
379
	 */
380
	public function isFieldVirtualised($field) {
381
		// Don't defer if field is non-virtualised
382
		$ignore = $this->getNonVirtualisedFields();
383
		if(in_array($field, $ignore)) {
384
			return false;
385
		}
386
387
		// Don't defer if no virtual page
388
		$copied = $this->CopyContentFrom();
389
		if(!$copied || !$copied->exists()) {
390
			return false;
391
	}
392
393
		// Check if copied object has this field
394
		return $copied->hasField($field);
395
	}
396
397
	/**
398
	 * Pass unrecognized method calls on to the original data object
399
	 *
400
	 * @param string $method
401
	 * @param string $args
402
	 * @return mixed
403
	 */
404
	public function __call($method, $args) {
405
		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...
406
			return parent::__call($method, $args);
407
		} else {
408
			return call_user_func_array(array($this->CopyContentFrom(), $method), $args);
409
		}
410
	}
411
412
	/**
413
	 * @param string $field
414
	 * @return bool
415
	 */
416
	public function hasField($field) {
417
		if(parent::hasField($field)) {
418
			return true;
419
		}
420
		$copy = $this->CopyContentFrom();
421
		return $copy && $copy->exists() && $copy->hasField($field);
422
	}
423
424
	/**
425
	 * Overwrite to also check for method on the original data object
426
	 *
427
	 * @param string $method
428
	 * @return bool
429
	 */
430
	public function hasMethod($method) {
431
		if(parent::hasMethod($method)) {
432
			return true;
433
		}
434
		// Don't call property setters on copied page
435
		if(stripos($method, 'set') === 0) {
436
			return false;
437
		}
438
		$copy = $this->CopyContentFrom();
439
		return $copy && $copy->exists() && $copy->hasMethod($method);
440
	}
441
442
	/**
443
	 * Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value object) for a field
444
	 * on this object.
445
	 *
446
	 * @param string $field
447
	 * @return string
448
	 */
449
	public function castingHelper($field) {
450
		$copy = $this->CopyContentFrom();
451
		if($copy && $copy->exists() && ($helper = $copy->castingHelper($field))) {
452
			return $helper;
453
		}
454
		return parent::castingHelper($field);
455
	}
456
457
}
458
459
/**
460
 * Controller for the virtual page.
461
 * @package cms
462
 */
463
class VirtualPage_Controller extends Page_Controller {
464
465
	private static $allowed_actions = array(
466
		'loadcontentall' => 'ADMIN',
467
	);
468
469
	/**
470
	 * Backup of virtualised controller
471
	 *
472
	 * @var ContentController
473
	 */
474
	protected $virtualController = null;
475
476
	/**
477
	 * Get virtual controller
478
	 *
479
	 * @return ContentController
480
	 */
481
	protected function getVirtualisedController() {
482
		if($this->virtualController) {
483
			return $this->virtualController;
484
		}
485
486
		// Validate virtualised model
487
		/** @var VirtualPage $page */
488
		$page = $this->data();
489
		$virtualisedPage = $page->CopyContentFrom();
490
		if (!$virtualisedPage || !$virtualisedPage->exists()) {
491
			return null;
492
		}
493
494
		// Create controller using standard mechanism
495
		$this->virtualController = ModelAsController::controller_for($virtualisedPage);
496
		return $this->virtualController;
497
	}
498
499
	public function getViewer($action) {
500
		$controller = $this->getVirtualisedController() ?: $this;
501
		return $controller->getViewer($action);
502
	}
503
504
	/**
505
	 * When the virtualpage is loaded, check to see if the versions are the same
506
	 * if not, reload the content.
507
	 * NOTE: Virtual page must have a container object of subclass of sitetree.
508
	 * We can't load the content without an ID or record to copy it from.
509
	 */
510
	public function init(){
511
		parent::init();
512
		$this->__call('init', array());
513
	}
514
515
	/**
516
	 * Also check the original object's original controller for the method
517
	 *
518
	 * @param string $method
519
	 * @return bool
520
	 */
521
	public function hasMethod($method) {
522
		if(parent::hasMethod($method)) {
523
			return true;
524
		};
525
526
		// Fallback
527
		$controller = $this->getVirtualisedController();
528
		return $controller && $controller->hasMethod($method);
529
	}
530
531
	/**
532
	 * Pass unrecognized method calls on to the original controller
533
	 *
534
	 * @param string $method
535
	 * @param string $args
536
	 * @return mixed
537
	 *
538
	 * @throws Exception Any error other than a 'no method' error.
539
	 */
540
	public function __call($method, $args)
541
	{
542
		// Check if we can safely call this method before passing it back
543
		// to custom methods.
544
		if($this->getExtraMethodConfig($method)) {
545
			return parent::__call($method, $args);
546
		}
547
548
		// Pass back to copied page
549
		$controller = $this->getVirtualisedController();
550
		if(!$controller) {
551
			return null;
552
		}
553
554
		// Ensure request/response data is available on virtual controller
555
		$controller->setRequest($this->getRequest());
556
		$controller->setResponse($this->getResponse());
557
558
		return call_user_func_array(array($controller, $method), $args);
559
	}
560
}
561