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