Completed
Pull Request — master (#1722)
by Robbie
02:13
created

VirtualPage::getFrontendControllerName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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