Completed
Pull Request — master (#1399)
by Ingo
02:39
created

VirtualPage::hasMethod()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 6
rs 9.4285
cc 2
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
* @package cms
7
*/
8
class VirtualPage extends Page {
9
10
	private static $description = 'Displays the content of another page';
11
	
12
	public static $virtualFields;
13
	
14
	/**
15
	 * @var array Define fields that are not virtual - the virtual page must define these fields themselves.
16
	 * Note that anything in {@link self::config()->initially_copied_fields} is implicitly included in this list.
17
	 */
18
	private static $non_virtual_fields = array(
19
		"ID",
20
		"ClassName",
21
		"SecurityTypeID",
22
		"OwnerID",
23
		"URLSegment",
24
		"Sort",
25
		"Status",
26
		'ShowInMenus',
27
		// 'Locale'
28
		'ShowInSearch',
29
		'Version',
30
		"Embargo",
31
		"Expiry",
32
		"CanViewType",
33
		"CanEditType",
34
	);
35
	
36
	/**
37
	 * @var array Define fields that are initially copied to virtual pages but left modifiable after that.
38
	 */
39
	private static $initially_copied_fields = array(
40
		'ShowInMenus',
41
		'ShowInSearch',
42
		'URLSegment',
43
	);
44
	
45
	private static $has_one = array(
46
		"CopyContentFrom" => "SiteTree",	
47
	);
48
	
49
	private static $db = array(
50
		"VersionID" => "Int",
51
	);
52
	
53
	/** 
54
	 * Generates the array of fields required for the page type.
55
	 */
56
	public function getVirtualFields() {
57
		$nonVirtualFields = array_merge(self::config()->non_virtual_fields, self::config()->initially_copied_fields);
58
		$record = $this->CopyContentFrom();
59
60
		$virtualFields = array();
61
		foreach($record->db() as $field => $type) {
0 ignored issues
show
Bug introduced by
The expression $record->db() of type array|string|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
62
			if(!in_array($field, $nonVirtualFields)) {
63
				$virtualFields[] = $field;
64
		}
65
		}
66
		return $virtualFields;
67
	}
68
69
	/**
70
	 * Returns the linked page, or failing that, a new object.
71
	 *
72
	 * Always returns a non-empty object
73
	 * 
74
	 * @return SiteTree 
75
	 */
76
	public function CopyContentFrom() {
77
		$copyContentFromID = $this->CopyContentFromID;
78
		if(!$copyContentFromID) {
79
			return new SiteTree();
80
		}
81
		
82
		if(!isset($this->components['CopyContentFrom'])) {
83
			$this->components['CopyContentFrom'] = DataObject::get_by_id("SiteTree", $copyContentFromID);
84
85
			// Don't let VirtualPages point to other VirtualPages
86
			if($this->components['CopyContentFrom'] instanceof VirtualPage) {
87
				$this->components['CopyContentFrom'] = null;
88
			}
89
				
90
			// has_one component semantics incidate than an empty object should be returned
91
			if(!$this->components['CopyContentFrom']) {
92
				$this->components['CopyContentFrom'] = new SiteTree();
93
			}
94
		}
95
		
96
		return $this->components['CopyContentFrom'] ? $this->components['CopyContentFrom'] : new SiteTree();
97
	}
98
	
99
	public function setCopyContentFromID($val) {
100
		if($val && DataObject::get_by_id('SiteTree', $val) instanceof VirtualPage) {
101
			$val = 0;
102
		}
103
		return $this->setField("CopyContentFromID", $val);
104
	}
105
 
106
	public function ContentSource() {
107
		return $this->CopyContentFrom();
108
	}
109
110
	/**
111
	 * For VirtualPage, add a canonical link tag linking to the original page
112
	 * See TRAC #6828 & http://support.google.com/webmasters/bin/answer.py?hl=en&answer=139394
113
	 *
114
	 * @param boolean $includeTitle Show default <title>-tag, set to false for custom templating
115
	 * @return string The XHTML metatags
116
	 */
117
	public function MetaTags($includeTitle = true) {
118
		$tags = parent::MetaTags($includeTitle);
119
		if ($this->CopyContentFrom()->ID) {
120
			$tags .= "<link rel=\"canonical\" href=\"{$this->CopyContentFrom()->Link()}\" />\n";
121
		}
122
		return $tags;
123
	}
124
	
125
	public function allowedChildren() {
126
		if($this->CopyContentFrom()) {
127
			return $this->CopyContentFrom()->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 getSettingsFields() {
241
		$fields = parent::getSettingsFields();
242
		if(!$this->CopyContentFrom()->exists()) {
243
			$fields->addFieldToTab("Root.Settings", 
244
				new LiteralField(
245
					'VirtualPageWarning',
246
					'<div class="message notice">'
247
					 . _t(
248
							'SITETREE.VIRTUALPAGEWARNINGSETTINGS',
249
							'Please choose a linked page in the main content fields in order to publish'
250
						)
251
					. '</div>'
252
				),
253
				'ClassName'
254
			);
255
		}
256
257
		return $fields;
258
	}
259
	
260
	/** 
261
	 * We have to change it to copy all the content from the original page first.
262
	 */
263
	public function onBeforeWrite() {
264
		$performCopyFrom = null;
0 ignored issues
show
Unused Code introduced by
$performCopyFrom is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
265
266
		// Determine if we need to copy values.
267
		if(
268
			$this->extension_instances['Versioned']->migratingVersion
269
			&& Versioned::current_stage() == 'Live'
270
			&& $this->CopyContentFromID
271
		) {
272
			// On publication to live, copy from published source.
273
			$performCopyFrom = true;
274
		
275
			$stageSourceVersion = DB::prepared_query(
276
				'SELECT "Version" FROM "SiteTree" WHERE "ID" = ?',
277
				array($this->CopyContentFromID)
278
			)->value();
279
			$liveSourceVersion = DB::prepared_query(
280
				'SELECT "Version" FROM "SiteTree_Live" WHERE "ID" = ?',
281
				array($this->CopyContentFromID)
282
			)->value();
283
		
284
			// We're going to create a new VP record in SiteTree_versions because the published
285
			// version might not exist, unless we're publishing the latest version
286
			if($stageSourceVersion != $liveSourceVersion) {
287
				$this->extension_instances['Versioned']->migratingVersion = null;
288
			}
289
		} else {
290
			// On regular write, copy from draft source. This is only executed when the source page changes.
291
			$performCopyFrom = $this->isChanged('CopyContentFromID', 2) && $this->CopyContentFromID != 0;
292
		}
293
		
294
 		if($performCopyFrom && $this instanceof VirtualPage) {
295
			// This flush is needed because the get_one cache doesn't respect site version :-(
296
			singleton('SiteTree')->flushCache();
297
			// @todo Update get_one to support parameterised queries
298
			$source = DataObject::get_by_id("SiteTree", $this->CopyContentFromID);
299
			// Leave the updating of image tracking until after write, in case its a new record
300
			$this->copyFrom($source, false);
301
		}
302
		
303
		parent::onBeforeWrite();
304
	}
305
	
306
	public function onAfterWrite() {
307
		parent::onAfterWrite();
308
309
		// Don't do this stuff when we're publishing
310
		if(!$this->extension_instances['Versioned']->migratingVersion) {
311
	 		if(
312
				$this->isChanged('CopyContentFromID')
313
	 			&& $this->CopyContentFromID != 0 
314
				&& $this instanceof VirtualPage
315
			) {
316
				$this->updateImageTracking();
317
			}
318
		}
319
320
		// Check if page type has changed to a non-virtual page.
321
		// Caution: Relies on the fact that the current instance is still of the old page type.
322
		if($this->isChanged('ClassName', 2)) {
323
			$changed = $this->getChangedFields();
324
			$classBefore = $changed['ClassName']['before'];
325
			$classAfter = $changed['ClassName']['after'];
326
			if($classBefore != $classAfter) {
327
				// Remove all database rows for the old page type to avoid inconsistent data retrieval.
328
				// TODO This should apply to all page type changes, not only on VirtualPage - but needs
329
				// more comprehensive testing as its a destructive operation
330
				$removedTables = array_diff(ClassInfo::dataClassesFor($classBefore), ClassInfo::dataClassesFor($classAfter));
331
				if($removedTables) foreach($removedTables as $removedTable) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $removedTables of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
332
					// Note: *_versions records are left intact
333
					foreach(array('', 'Live') as $stage) {
334
						if($stage) $removedTable = "{$removedTable}_{$stage}";
335
						DB::prepared_query("DELETE FROM \"$removedTable\" WHERE \"ID\" = ?", array($this->ID));
336
					}
337
				}
338
339
				// Also publish the change immediately to avoid inconsistent behaviour between
340
				// a non-virtual draft and a virtual live record (e.g. republishing the original record
341
				// shouldn't republish the - now unrelated - changes on the ex-VirtualPage draft).
342
				// Copies all stage fields to live as well.
343
				// @todo Update get_one to support parameterised queries
344
				$source = DataObject::get_by_id("SiteTree", $this->CopyContentFromID);
345
				$this->copyFrom($source);
346
				$this->publish('Stage', 'Live');
347
348
				// Change reference on instance (as well as removing the underlying database tables)
349
				$this->CopyContentFromID = 0;
350
			}
351
		}
352
	}
353
354
	public function validate() {
355
		$result = parent::validate();
356
357
		// "Can be root" validation
358
		$orig = $this->CopyContentFrom();
359 View Code Duplication
		if(!$orig->stat('can_be_root') && !$this->ParentID) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
360
			$result->error(
361
				_t(
362
					'VirtualPage.PageTypNotAllowedOnRoot', 
363
					'Original page type "{type}" is not allowed on the root level for this virtual page', 
364
					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...
365
				),
366
				'CAN_BE_ROOT_VIRTUAL'
367
			);
368
		}
369
370
		return $result;
371
	}
372
	
373
	/**
374
	 * Ensure we have an up-to-date version of everything.
375
	 *
376
	 * @param DataObject $source
377
	 * @param bool $updateImageTracking
378
	 */
379
	public function copyFrom($source, $updateImageTracking = true) {
380
		if($source) {
381
			foreach($this->getVirtualFields() as $virtualField) {
382
				$this->$virtualField = $source->$virtualField;
383
			}
384
			
385
			// We also want to copy certain, but only if we're copying the source page for the first
386
			// time. After this point, the user is free to customise these for the virtual page themselves.
387
			if($this->isChanged('CopyContentFromID', 2) && $this->CopyContentFromID != 0) {
388
				foreach(self::config()->initially_copied_fields as $fieldName) {
389
					$this->$fieldName = $source->$fieldName;
390
				}
391
			}
392
			
393
			if($updateImageTracking) $this->updateImageTracking();
394
		}
395
	}
396
	
397
	public function updateImageTracking() {
398
		// Doesn't work on unsaved records
399
		if(!$this->ID) return;
400
401
		// Remove CopyContentFrom() from the cache
402
		unset($this->components['CopyContentFrom']);
403
		
404
		// Update ImageTracking
405
		$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...
406
	}
407
408
	/**
409
	 * @param string $numChildrenMethod
410
	 * @return string
411
	 */
412
	public function CMSTreeClasses($numChildrenMethod="numChildren") {
413
		return parent::CMSTreeClasses($numChildrenMethod) . ' VirtualPage-' . $this->CopyContentFrom()->ClassName;
414
	}
415
	
416
	/**
417
	 * Allow attributes on the master page to pass
418
	 * through to the virtual page
419
	 *
420
	 * @param string $field 
421
	 * @return mixed
422
	 */
423
	public function __get($field) {
424
		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...
425
			return $this->$funcName();
426
		} 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...
427
			return $this->getField($field);
428
		} else {
429
			return $this->copyContentFrom()->$field;
430
		}
431
	}
432
	
433
	/**
434
	 * Pass unrecognized method calls on to the original data object
435
	 *
436
	 * @param string $method 
437
	 * @param string $args
438
	 * @return mixed
439
	 */
440
	public function __call($method, $args) {
441
		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...
442
			return parent::__call($method, $args);
443
		} else {
444
			return call_user_func_array(array($this->copyContentFrom(), $method), $args);
445
		}
446
	}
447
448
	/**
449
	 * @param string $field
450
	 * @return bool
451
	 */
452
	public function hasField($field) {
453
		if(parent::hasField($field)) {
454
			return true;
455
	}	
456
		return $this->CopyContentFrom()->hasField($field);
457
	}	
458
	/**
459
	 * Overwrite to also check for method on the original data object
460
	 *
461
	 * @param string $method 
462
	 * @return bool 
463
	 */
464
	public function hasMethod($method) {
465
		if(parent::hasMethod($method)) {
466
			return true;
467
	}
468
		return $this->CopyContentFrom()->hasMethod($method);
469
	}
470
471
	/**
472
	 * Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value object) for a field
473
	 * on this object.
474
	 *
475
	 * @param string $field
476
	 * @return string
477
	 */
478
	public function castingHelper($field) {
479
		return $this
480
			->CopyContentFrom()
481
			->castingHelper($field);
482
	}
483
484
}
485
486
/**
487
 * Controller for the virtual page.
488
 * @package cms
489
 */
490
class VirtualPage_Controller extends Page_Controller {
491
	
492
	private static $allowed_actions = array(
493
		'loadcontentall' => 'ADMIN',
494
	);
495
	
496
	/**
497
	 * Reloads the content if the version is different ;-)
498
	 */
499
	public function reloadContent() {
500
		$this->failover->copyFrom($this->failover->CopyContentFrom());
501
		$this->failover->write();
502
		return;
503
	}
504
	
505
	public function getViewer($action) {
506
		$originalClass = get_class($this->CopyContentFrom());
507
		if ($originalClass == 'SiteTree') $name = 'Page_Controller';
508
		else $name = $originalClass."_Controller";
509
		$controller = new $name();
510
		return $controller->getViewer($action);
511
	}
512
	
513
	/**
514
	 * When the virtualpage is loaded, check to see if the versions are the same
515
	 * if not, reload the content.
516
	 * NOTE: Virtual page must have a container object of subclass of sitetree.
517
	 * We can't load the content without an ID or record to copy it from.
518
	 */
519
	public function init(){
520
		if(isset($this->record) && $this->record->ID){
521
			if($this->record->VersionID != $this->failover->CopyContentFrom()->Version){
522
				$this->reloadContent();
523
				$this->VersionID = $this->failover->CopyContentFrom()->VersionID;
524
			}
525
		}
526
		parent::init();
527
		$this->__call('init', array());
528
	}
529
530
	public function loadcontentall() {
531
		$pages = DataObject::get("VirtualPage");
532
		foreach($pages as $page) {
533
			$page->copyFrom($page->CopyContentFrom());
534
			$page->write();
535
			$page->publish("Stage", "Live");
536
			echo "<li>Published $page->URLSegment";
537
		}
538
	}
539
	
540
	/**
541
	 * Also check the original object's original controller for the method
542
	 *
543
	 * @param string $method 
544
	 * @return bool 
545
	 */
546
	public function hasMethod($method) {
547
		$haveIt = parent::hasMethod($method);
548
		if (!$haveIt) {	
549
			$originalClass = get_class($this->CopyContentFrom());
550
			if ($originalClass == 'SiteTree') $name = 'ContentController';
551
			else $name = $originalClass."_Controller";
552
			$controller = new $name($this->dataRecord->copyContentFrom());
553
			$haveIt = $controller->hasMethod($method);
554
		}
555
		return $haveIt;
556
	}
557
	
558
	/**
559
	 * Pass unrecognized method calls on to the original controller
560
	 *
561
	 * @param string $method 
562
	 * @param string $args
563
	 * @return mixed
564
	 *
565
	 * @throws Exception Any error other than a 'no method' error.
566
	 */
567
	public function __call($method, $args) {
568
		try {
569
			return parent::__call($method, $args);
570
		} catch (Exception $e) {
571
			// Hack... detect exception type. We really should use exception subclasses.
572
			// if the exception isn't a 'no method' error, rethrow it
573
			if ($e->getCode() !== 2175) {
574
				throw $e;
575
			}
576
577
			$original = $this->copyContentFrom();
578
			$controller = ModelAsController::controller_for($original);
579
580
			// Ensure request/response data is available on virtual controller
581
			$controller->setRequest($this->getRequest());
582
			$controller->response = $this->response; // @todo - replace with getter/setter in 3.3
583
584
			return call_user_func_array(array($controller, $method), $args);
585
		}
586
	}
587
}
588
589
590