Completed
Pull Request — master (#1605)
by Loz
03:02
created

VirtualPage::getControllerName()   A

Complexity

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