Completed
Push — master ( e56e4c...25a8fb )
by Hamish
02:36
created

VirtualPage_Controller::getVirtualisedController()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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