Completed
Push — master ( 849cd8...301702 )
by Hamish
18s
created

VirtualPage::hasField()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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