Completed
Push — master ( 25a8fb...29fc7a )
by Hamish
12s
created

CMSPageHistoryController   C

Complexity

Total Complexity 48

Size/Duplication

Total Lines 433
Duplicated Lines 10.85 %

Coupling/Cohesion

Components 1
Dependencies 18

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 48
lcom 1
cbo 18
dl 47
loc 433
rs 5.3384
c 2
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A getResponseNegotiator() 0 13 2
A show() 14 14 2
A compare() 17 17 2
A getSilverStripeNavigator() 0 9 2
C getEditForm() 0 76 9
C VersionsForm() 0 84 9
B doCompare() 8 28 3
B doShowVersion() 8 27 5
A ShowVersionForm() 0 8 2
C CompareVersionsForm() 0 61 11
A Breadcrumbs() 0 5 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like CMSPageHistoryController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CMSPageHistoryController, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\CMS\Controllers;
4
5
use SilverStripe\CMS\Model\SiteTree;
6
use SilverStripe\ORM\FieldType\DBField;
7
use SilverStripe\ORM\FieldType\DBHTMLText;
8
use SilverStripe\ORM\Versioning\Versioned;
9
use SilverStripe\Security\Security;
10
use Form;
11
use FieldList;
12
use FormAction;
13
use HiddenField;
14
use Controller;
15
use LiteralField;
16
use SS_HTTPRequest;
17
use SS_HTTPResponse;
18
use ViewableData;
19
use CheckboxField;
20
21
/**
22
 * @package cms
23
 * @subpackage controllers
24
 */
25
class CMSPageHistoryController extends CMSMain {
26
27
	private static $url_segment = 'pages/history';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
28
29
	private static $url_rule = '/$Action/$ID/$VersionID/$OtherVersionID';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
30
31
	private static $url_priority = 42;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
32
33
	private static $menu_title = 'History';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
34
35
	private static $required_permission_codes = 'CMS_ACCESS_CMSMain';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
36
37
	private static $allowed_actions = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
38
		'VersionsForm',
39
		'CompareVersionsForm',
40
		'show',
41
		'compare'
42
	);
43
44
	private static $url_handlers = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
45
		'$Action/$ID/$VersionID/$OtherVersionID' => 'handleAction'
46
	);
47
48
	public function getResponseNegotiator() {
49
		$negotiator = parent::getResponseNegotiator();
50
		$controller = $this;
51
		$negotiator->setCallback('CurrentForm', function() use(&$controller) {
52
			$form = $controller->ShowVersionForm($controller->getRequest()->param('VersionID'));
53
			if($form) return $form->forTemplate();
54
			else return $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
55
		});
56
		$negotiator->setCallback('default', function() use(&$controller) {
57
			return $controller->renderWith($controller->getViewer('show'));
58
		});
59
		return $negotiator;
60
	}
61
62
	/**
63
	 * @param SS_HTTPRequest $request
64
	 * @return array
65
	 */
66 View Code Duplication
	public function show($request) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
67
		$form = $this->ShowVersionForm($request->param('VersionID'));
68
69
		$negotiator = $this->getResponseNegotiator();
70
		$controller = $this;
71
		$negotiator->setCallback('CurrentForm', function() use(&$controller, &$form) {
72
			return $form ? $form->forTemplate() : $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
73
		});
74
		$negotiator->setCallback('default', function() use(&$controller, &$form) {
75
			return $controller->customise(array('EditForm' => $form))->renderWith($controller->getViewer('show'));
76
		});
77
78
		return $negotiator->respond($request);
79
	}
80
81
	/**
82
	 * @param SS_HTTPRequest $request
83
	 * @return array
84
	 */
85 View Code Duplication
	public function compare($request) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
86
		$form = $this->CompareVersionsForm(
87
			$request->param('VersionID'),
88
			$request->param('OtherVersionID')
89
		);
90
91
		$negotiator = $this->getResponseNegotiator();
92
		$controller = $this;
93
		$negotiator->setCallback('CurrentForm', function() use(&$controller, &$form) {
94
			return $form ? $form->forTemplate() : $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
95
		});
96
		$negotiator->setCallback('default', function() use(&$controller, &$form) {
97
			return $controller->customise(array('EditForm' => $form))->renderWith($controller->getViewer('show'));
98
		});
99
100
		return $negotiator->respond($request);
101
	}
102
103
	public function getSilverStripeNavigator() {
104
		$record = $this->getRecord($this->currentPageID(), $this->getRequest()->param('VersionID'));
105
		if($record) {
106
			$navigator = new SilverStripeNavigator($record);
107
			return $navigator->renderWith($this->getTemplatesWithSuffix('_SilverStripeNavigator'));
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $navigator->rende...lverStripeNavigator')); (SilverStripe\ORM\FieldType\DBHTMLText) is incompatible with the return type of the parent method LeftAndMain::getSilverStripeNavigator of type ArrayData.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
108
		} else {
109
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type of the parent method LeftAndMain::getSilverStripeNavigator of type ArrayData.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
110
		}
111
	}
112
113
	/**
114
	 * Returns the read only version of the edit form. Detaches all {@link FormAction}
115
	 * instances attached since only action relates to revert.
116
	 *
117
	 * Permission checking is done at the {@link CMSMain::getEditForm()} level.
118
	 *
119
	 * @param int $id ID of the record to show
120
	 * @param array $fields optional
121
	 * @param int $versionID
122
	 * @param int $compareID Compare mode
123
	 *
124
	 * @return Form
125
	 */
126
	public function getEditForm($id = null, $fields = null, $versionID = null, $compareID = null) {
127
		if(!$id) $id = $this->currentPageID();
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
128
129
		$record = $this->getRecord($id, $versionID);
130
		$versionID = ($record) ? $record->Version : $versionID;
0 ignored issues
show
Bug introduced by
The property Version does not seem to exist. Did you mean versioning?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
131
132
		$form = parent::getEditForm($record, ($record) ? $record->getCMSFields() : null);
0 ignored issues
show
Documentation introduced by
$record is of type object<SilverStripe\CMS\Model\SiteTree>, but the function expects a integer|null.

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...
133
		// Respect permission failures from parent implementation
134
		if(!($form instanceof Form)) return $form;
135
136
		// TODO: move to the SilverStripeNavigator structure so the new preview can pick it up.
137
		//$nav = new SilverStripeNavigatorItem_ArchiveLink($record);
138
139
		$form->setActions(new FieldList(
140
			$revert = FormAction::create('doRollback', _t('CMSPageHistoryController.REVERTTOTHISVERSION', 'Revert to this version'))->setUseButtonTag(true)
141
		));
142
143
		$fields = $form->Fields();
144
		$fields->removeByName("Status");
145
		$fields->push(new HiddenField("ID"));
146
		$fields->push(new HiddenField("Version"));
147
148
		$fields = $fields->makeReadonly();
149
150
		if($compareID) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $compareID of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
151
			$link = Controller::join_links(
152
				$this->Link('show'),
153
				$id
154
			);
155
156
			$view = _t('CMSPageHistoryController.VIEW',"view");
157
158
			$message = _t(
159
				'CMSPageHistoryController.COMPARINGVERSION',
160
				"Comparing versions {version1} and {version2}.",
161
				array(
0 ignored issues
show
Documentation introduced by
array('version1' => spri...k, $compareID), $view)) is of type array<string,string,{"ve...","version2":"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...
162
					'version1' => sprintf('%s (<a href="%s">%s</a>)', $versionID, Controller::join_links($link, $versionID), $view),
163
					'version2' => sprintf('%s (<a href="%s">%s</a>)', $compareID, Controller::join_links($link, $compareID), $view)
164
				)
165
			);
166
167
			$revert->setReadonly(true);
168
		} else {
169
			if($record->isLatestVersion()) {
0 ignored issues
show
Documentation Bug introduced by
The method isLatestVersion 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...
170
				$message = _t('CMSPageHistoryController.VIEWINGLATEST', 'Currently viewing the latest version.');
171
			} else {
172
				$message = _t(
173
					'CMSPageHistoryController.VIEWINGVERSION',
174
					"Currently viewing version {version}.",
175
					array('version' => $versionID)
0 ignored issues
show
Documentation introduced by
array('version' => $versionID) is of type array<string,?,{"version":"?"}>, 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...
176
				);
177
			}
178
		}
179
180
		$fields->addFieldToTab('Root.Main',
181
			new LiteralField('CurrentlyViewingMessage', $this->customise(array(
182
				'Content' => DBField::create_field('HTMLFragment', $message),
183
				'Classes' => 'notice'
184
			))->renderWith(array('CMSMain_notice'))),
185
			"Title"
186
		);
187
188
		$form->setFields($fields->makeReadonly());
189
		$form->loadDataFrom(array(
190
			"ID" => $id,
191
			"Version" => $versionID,
192
		));
193
194
		if(($record && $record->isLatestVersion())) {
0 ignored issues
show
Documentation Bug introduced by
The method isLatestVersion 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...
195
			$revert->setReadonly(true);
196
		}
197
198
		$form->removeExtraClass('cms-content');
199
200
		return $form;
201
	}
202
203
204
	/**
205
	 * Version select form. Main interface between selecting versions to view
206
	 * and comparing multiple versions.
207
	 *
208
	 * Because we can reload the page directly to a compare view (history/compare/1/2/3)
209
	 * this form has to adapt to those parameters as well.
210
	 *
211
	 * @return Form
212
	 */
213
	public function VersionsForm() {
214
		$id = $this->currentPageID();
215
		$page = $this->getRecord($id);
216
		$versionsHtml = '';
217
218
		$action = $this->getRequest()->param('Action');
219
		$versionID = $this->getRequest()->param('VersionID');
220
		$otherVersionID = $this->getRequest()->param('OtherVersionID');
221
222
		$showUnpublishedChecked = 0;
223
		$compareModeChecked = ($action == "compare");
224
225
		if($page) {
226
			$versions = $page->allVersions();
0 ignored issues
show
Documentation Bug introduced by
The method allVersions 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...
227
			$versionID = (!$versionID) ? $page->Version : $versionID;
0 ignored issues
show
Bug introduced by
The property Version does not seem to exist. Did you mean versioning?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
228
229
			if($versions) {
230
				foreach($versions as $k => $version) {
231
					$active = false;
232
233
					if($version->Version == $versionID || $version->Version == $otherVersionID) {
234
						$active = true;
235
236
						if(!$version->WasPublished) $showUnpublishedChecked = 1;
237
					}
238
239
					$version->Active = ($active);
240
				}
241
			}
242
243
			$vd = new ViewableData();
244
245
			$versionsHtml = $vd->customise(array(
246
				'Versions' => $versions
247
			))->renderWith($this->getTemplatesWithSuffix('_versions'));
248
		}
249
250
		$fields = new FieldList(
251
			new CheckboxField(
252
				'ShowUnpublished',
253
				_t('CMSPageHistoryController.SHOWUNPUBLISHED','Show unpublished versions'),
254
				$showUnpublishedChecked
255
			),
256
			new CheckboxField(
257
				'CompareMode',
258
				_t('CMSPageHistoryController.COMPAREMODE', 'Compare mode (select two)'),
259
				$compareModeChecked
260
			),
261
			new LiteralField('VersionsHtml', $versionsHtml),
0 ignored issues
show
Bug introduced by
It seems like $versionsHtml defined by $vd->customise(array('Ve...ithSuffix('_versions')) on line 245 can also be of type object<SilverStripe\ORM\FieldType\DBHTMLText>; however, LiteralField::__construct() does only seem to accept string|object<FormField>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
262
			$hiddenID = new HiddenField('ID', false, "")
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a null|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...
263
		);
264
265
		$actions = new FieldList(
266
			new FormAction(
267
				'doCompare', _t('CMSPageHistoryController.COMPAREVERSIONS','Compare Versions')
268
			),
269
			new FormAction(
270
				'doShowVersion', _t('CMSPageHistoryController.SHOWVERSION','Show Version')
271
			)
272
		);
273
274
		// Use <button> to allow full jQuery UI styling
275
		foreach($actions->dataFields() as $action) {
276
			/** @var FormAction $action */
277
			$action->setUseButtonTag(true);
278
		}
279
280
		$form = Form::create(
281
			$this,
282
			'VersionsForm',
283
			$fields,
284
			$actions
285
		)->setHTMLID('Form_VersionsForm');
286
		$form->loadDataFrom($this->getRequest()->requestVars());
0 ignored issues
show
Bug introduced by
It seems like $this->getRequest()->requestVars() targeting SS_HTTPRequest::requestVars() can also be of type null; however, Form::loadDataFrom() does only seem to accept array|object<SilverStripe\ORM\DataObject>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
287
		$hiddenID->setValue($id);
288
		$form->unsetValidator();
289
290
		$form
291
			->addExtraClass('cms-versions-form') // placeholder, necessary for $.metadata() to work
292
			->setAttribute('data-link-tmpl-compare', Controller::join_links($this->Link('compare'), '%s', '%s', '%s'))
293
			->setAttribute('data-link-tmpl-show', Controller::join_links($this->Link('show'), '%s', '%s'));
294
295
		return $form;
296
	}
297
298
	/**
299
	 * Process the {@link VersionsForm} compare function between two pages.
300
	 *
301
	 * @param array $data
302
	 * @param Form $form
303
	 * @return SS_HTTPResponse|DBHTMLText
304
	 */
305
	public function doCompare($data, $form) {
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
306
		$versions = $data['Versions'];
307
		if(count($versions) < 2) {
308
			return null;
309
		}
310
311
		$version1 = array_shift($versions);
312
		$version2 = array_shift($versions);
313
314
		$form = $this->CompareVersionsForm($version1, $version2);
315
316
		// javascript solution, render into template
317 View Code Duplication
		if($this->getRequest()->isAjax()) {
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...
318
			return $this->customise(array(
319
				"EditForm" => $form
320
			))->renderWith(array(
321
				$this->class . '_EditForm',
322
				'LeftAndMain_Content'
323
			));
324
		}
325
326
		// non javascript, redirect the user to the page
327
		return $this->redirect(Controller::join_links(
328
			$this->Link('compare'),
329
			$version1,
330
			$version2
331
		));
332
	}
333
334
	/**
335
	 * Process the {@link VersionsForm} show version function. Only requires
336
	 * one page to be selected.
337
	 *
338
	 * @param array
339
	 * @param Form
340
	 *
341
	 * @return DBHTMLText|SS_HTTPResponse
342
	 */
343
	public function doShowVersion($data, $form) {
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
344
		$versionID = null;
345
346
		if(isset($data['Versions']) && is_array($data['Versions'])) {
347
			$versionID  = array_shift($data['Versions']);
348
		}
349
350
		if(!$versionID) {
351
			return null;
352
		}
353
354
		$request = $this->getRequest();
355 View Code Duplication
		if($request->isAjax()) {
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...
356
			return $this->customise(array(
357
				"EditForm" => $this->ShowVersionForm($versionID)
358
			))->renderWith(array(
359
				$this->class . '_EditForm',
360
				'LeftAndMain_Content'
361
			));
362
		}
363
364
		// non javascript, redirect the user to the page
365
		return $this->redirect(Controller::join_links(
366
			$this->Link('version'),
367
			$versionID
368
		));
369
	}
370
371
	/**
372
	 * @param int|null $versionID
373
	 * @return Form
374
	 */
375
	public function ShowVersionForm($versionID = null) {
376
		if(!$versionID) return null;
0 ignored issues
show
Bug Best Practice introduced by
The expression $versionID of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
377
378
		$id = $this->currentPageID();
379
		$form = $this->getEditForm($id, null, $versionID);
380
381
		return $form;
382
	}
383
384
	/**
385
	 * @param int $versionID
386
	 * @param int $otherVersionID
387
	 * @return mixed
388
	 */
389
	public function CompareVersionsForm($versionID, $otherVersionID) {
390
		if($versionID > $otherVersionID) {
391
			$toVersion = $versionID;
392
			$fromVersion = $otherVersionID;
393
		} else {
394
			$toVersion = $otherVersionID;
395
			$fromVersion = $versionID;
396
		}
397
398
		if(!$toVersion || !$fromVersion) {
399
			return null;
400
		}
401
402
		$id = $this->currentPageID();
403
		/** @var SiteTree $page */
404
		$page = SiteTree::get()->byID($id);
405
406
 		if($page && $page->exists()) {
407
			if(!$page->canView()) {
408
				return Security::permissionFailure($this);
409
			}
410
411
			$record = $page->compareVersions($fromVersion, $toVersion);
0 ignored issues
show
Documentation Bug introduced by
The method compareVersions 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...
412
		}
413
414
		$fromVersionRecord = Versioned::get_version('SilverStripe\\CMS\\Model\\SiteTree', $id, $fromVersion);
415
		$toVersionRecord = Versioned::get_version('SilverStripe\\CMS\\Model\\SiteTree', $id, $toVersion);
416
417
		if(!$fromVersionRecord) {
418
			user_error("Can't find version $fromVersion of page $id", E_USER_ERROR);
419
		}
420
421
		if(!$toVersionRecord) {
422
			user_error("Can't find version $toVersion of page $id", E_USER_ERROR);
423
		}
424
425
		if(!$record) {
0 ignored issues
show
Bug introduced by
The variable $record does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
426
			return null;
427
		}
428
		$form = $this->getEditForm($id, null, null, true);
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a integer|null.

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...
429
		$form->setActions(new FieldList());
430
		$form->addExtraClass('compare');
431
432
		// Comparison views shouldn't be editable.
433
		// Its important to convert fields *before* loading data,
434
		// as the comparison output is HTML and not valid values for the various field types
435
		$readonlyFields = $form->Fields()->makeReadonly();
436
		$form->setFields($readonlyFields);
437
438
		$form->loadDataFrom($record);
439
		$form->loadDataFrom(array(
440
			"ID" => $id,
441
			"Version" => $fromVersion,
442
		));
443
444
		foreach($form->Fields()->dataFields() as $field) {
445
			$field->dontEscape = true;
446
		}
447
448
		return $form;
449
	}
450
451
	public function Breadcrumbs($unlinked = false) {
452
		$crumbs = parent::Breadcrumbs($unlinked);
453
		$crumbs[0]->Title = _t('CMSPagesController.MENUTITLE');
454
		return $crumbs;
455
	}
456
457
}
458