Completed
Push — master ( bf5119...d208a6 )
by Ingo
03:17
created

CMSPageHistoryController   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 419
Duplicated Lines 11.22 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 47
c 2
b 0
f 0
lcom 1
cbo 15
dl 47
loc 419
rs 7.4481

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 82 9
B doCompare() 8 27 3
B doShowVersion() 8 24 5
A ShowVersionForm() 0 8 2
C CompareVersionsForm() 0 55 10
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
use SilverStripe\ORM\DataObject;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, DataObject.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
4
use SilverStripe\ORM\Versioning\Versioned;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Versioned.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
5
6
/**
7
 * @package cms
8
 * @subpackage controllers
9
 */
10
class CMSPageHistoryController extends CMSMain {
11
12
	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...
13
	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...
14
	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...
15
	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...
16
	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...
17
	private static $session_namespace = '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...
18
19
	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...
20
		'VersionsForm',
21
		'CompareVersionsForm',
22
		'show',
23
		'compare'
24
	);
25
26
	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...
27
		'$Action/$ID/$VersionID/$OtherVersionID' => 'handleAction'
28
	);
29
30
	public function getResponseNegotiator() {
31
		$negotiator = parent::getResponseNegotiator();
32
		$controller = $this;
33
		$negotiator->setCallback('CurrentForm', function() use(&$controller) {
34
			$form = $controller->ShowVersionForm($controller->getRequest()->param('VersionID'));
35
			if($form) return $form->forTemplate();
36
			else return $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
37
		});
38
		$negotiator->setCallback('default', function() use(&$controller) {
39
			return $controller->renderWith($controller->getViewer('show'));
40
		});
41
		return $negotiator;
42
	}
43
44
	/**
45
	 * @param SS_HTTPRequest $request
46
	 * @return array
47
	 */
48 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...
49
		$form = $this->ShowVersionForm($request->param('VersionID'));
50
51
		$negotiator = $this->getResponseNegotiator();
52
		$controller = $this;
53
		$negotiator->setCallback('CurrentForm', function() use(&$controller, &$form) {
54
			return $form ? $form->forTemplate() : $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
55
		});
56
		$negotiator->setCallback('default', function() use(&$controller, &$form) {
57
			return $controller->customise(array('EditForm' => $form))->renderWith($controller->getViewer('show'));
58
		});
59
60
		return $negotiator->respond($request);
61
	}
62
63
	/**
64
	 * @param SS_HTTPRequest $request
65
	 * @return array
66
	 */
67 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...
68
		$form = $this->CompareVersionsForm(
69
			$request->param('VersionID'),
70
			$request->param('OtherVersionID')
71
		);
72
73
		$negotiator = $this->getResponseNegotiator();
74
		$controller = $this;
75
		$negotiator->setCallback('CurrentForm', function() use(&$controller, &$form) {
76
			return $form ? $form->forTemplate() : $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
77
		});
78
		$negotiator->setCallback('default', function() use(&$controller, &$form) {
79
			return $controller->customise(array('EditForm' => $form))->renderWith($controller->getViewer('show'));
80
		});
81
82
		return $negotiator->respond($request);
83
	}
84
85
	public function getSilverStripeNavigator() {
86
		$record = $this->getRecord($this->currentPageID(), $this->getRequest()->param('VersionID'));
87
		if($record) {
88
			$navigator = new SilverStripeNavigator($record);
89
			return $navigator->renderWith($this->getTemplatesWithSuffix('_SilverStripeNavigator'));
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $navigator->rende...lverStripeNavigator')); (SilverStripe\Model\FieldType\DBField) 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...
90
		} else {
91
			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...
92
		}
93
	}
94
95
	/**
96
	 * Returns the read only version of the edit form. Detaches all {@link FormAction}
97
	 * instances attached since only action relates to revert.
98
	 *
99
	 * Permission checking is done at the {@link CMSMain::getEditForm()} level.
100
	 *
101
	 * @param int $id ID of the record to show
102
	 * @param array $fields optional
103
	 * @param int $versionID
104
	 * @param int $compareID Compare mode
105
	 *
106
	 * @return Form
107
	 */
108
	public function getEditForm($id = null, $fields = null, $versionID = null, $compareID = null) {
109
		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...
110
111
		$record = $this->getRecord($id, $versionID);
112
		$versionID = ($record) ? $record->Version : $versionID;
113
114
		$form = parent::getEditForm($record, ($record) ? $record->getCMSFields() : null);
0 ignored issues
show
Documentation introduced by
$record is of type object<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...
115
		// Respect permission failures from parent implementation
116
		if(!($form instanceof Form)) return $form;
117
118
		// TODO: move to the SilverStripeNavigator structure so the new preview can pick it up.
119
		//$nav = new SilverStripeNavigatorItem_ArchiveLink($record);
120
121
		$form->setActions(new FieldList(
122
			$revert = FormAction::create('doRollback', _t('CMSPageHistoryController.REVERTTOTHISVERSION', 'Revert to this version'))->setUseButtonTag(true)
123
		));
124
125
		$fields = $form->Fields();
126
		$fields->removeByName("Status");
127
		$fields->push(new HiddenField("ID"));
128
		$fields->push(new HiddenField("Version"));
129
130
		$fields = $fields->makeReadonly();
131
132
		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...
133
			$link = Controller::join_links(
134
				$this->Link('show'),
135
				$id
136
			);
137
138
			$view = _t('CMSPageHistoryController.VIEW',"view");
139
140
			$message = _t(
141
				'CMSPageHistoryController.COMPARINGVERSION',
142
				"Comparing versions {version1} and {version2}.",
143
				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...
144
					'version1' => sprintf('%s (<a href="%s">%s</a>)', $versionID, Controller::join_links($link, $versionID), $view),
145
					'version2' => sprintf('%s (<a href="%s">%s</a>)', $compareID, Controller::join_links($link, $compareID), $view)
146
				)
147
			);
148
149
			$revert->setReadonly(true);
150
		} else {
151
			if($record->isLatestVersion()) {
152
				$message = _t('CMSPageHistoryController.VIEWINGLATEST', 'Currently viewing the latest version.');
153
			} else {
154
				$message = _t(
155
					'CMSPageHistoryController.VIEWINGVERSION',
156
					"Currently viewing version {version}.",
157
					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...
158
				);
159
			}
160
		}
161
162
		$fields->addFieldToTab('Root.Main',
163
			new LiteralField('CurrentlyViewingMessage', $this->customise(array(
164
				'Content' => $message,
165
				'Classes' => 'notice'
166
			))->renderWith(array('CMSMain_notice'))),
167
			"Title"
168
		);
169
170
		$form->setFields($fields->makeReadonly());
171
		$form->loadDataFrom(array(
172
			"ID" => $id,
173
			"Version" => $versionID,
174
		));
175
176
		if(($record && $record->isLatestVersion())) {
177
			$revert->setReadonly(true);
178
		}
179
180
		$form->removeExtraClass('cms-content');
181
182
		return $form;
183
	}
184
185
186
	/**
187
	 * Version select form. Main interface between selecting versions to view
188
	 * and comparing multiple versions.
189
	 *
190
	 * Because we can reload the page directly to a compare view (history/compare/1/2/3)
191
	 * this form has to adapt to those parameters as well.
192
	 *
193
	 * @return Form
194
	 */
195
	public function VersionsForm() {
196
		$id = $this->currentPageID();
197
		$page = $this->getRecord($id);
198
		$versionsHtml = '';
199
200
		$action = $this->getRequest()->param('Action');
201
		$versionID = $this->getRequest()->param('VersionID');
202
		$otherVersionID = $this->getRequest()->param('OtherVersionID');
203
204
		$showUnpublishedChecked = 0;
205
		$compareModeChecked = ($action == "compare");
206
207
		if($page) {
208
			$versions = $page->allVersions();
209
			$versionID = (!$versionID) ? $page->Version : $versionID;
210
211
			if($versions) {
212
				foreach($versions as $k => $version) {
213
					$active = false;
214
215
					if($version->Version == $versionID || $version->Version == $otherVersionID) {
216
						$active = true;
217
218
						if(!$version->WasPublished) $showUnpublishedChecked = 1;
219
					}
220
221
					$version->Active = ($active);
222
				}
223
			}
224
225
			$vd = new ViewableData();
226
227
			$versionsHtml = $vd->customise(array(
228
				'Versions' => $versions
229
			))->renderWith('CMSPageHistoryController_versions');
230
		}
231
232
		$fields = new FieldList(
233
			new CheckboxField(
234
				'ShowUnpublished',
235
				_t('CMSPageHistoryController.SHOWUNPUBLISHED','Show unpublished versions'),
236
				$showUnpublishedChecked
237
			),
238
			new CheckboxField(
239
				'CompareMode',
240
				_t('CMSPageHistoryController.COMPAREMODE', 'Compare mode (select two)'),
241
				$compareModeChecked
242
			),
243
			new LiteralField('VersionsHtml', $versionsHtml),
0 ignored issues
show
Bug introduced by
It seems like $versionsHtml defined by $vd->customise(array('Ve...ryController_versions') on line 227 can also be of type object<SilverStripe\Model\FieldType\DBField>; 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...
244
			$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...
245
		);
246
247
		$actions = new FieldList(
248
			new FormAction(
249
				'doCompare', _t('CMSPageHistoryController.COMPAREVERSIONS','Compare Versions')
250
			),
251
			new FormAction(
252
				'doShowVersion', _t('CMSPageHistoryController.SHOWVERSION','Show Version')
253
			)
254
		);
255
256
		// Use <button> to allow full jQuery UI styling
257
		foreach($actions->dataFields() as $action) $action->setUseButtonTag(true);
258
259
		$negotiator = $this->getResponseNegotiator();
0 ignored issues
show
Unused Code introduced by
$negotiator 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...
260
		$form = Form::create(
261
			$this,
262
			'VersionsForm',
263
			$fields,
264
			$actions
265
		)->setHTMLID('Form_VersionsForm');
266
		$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<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...
267
		$hiddenID->setValue($id);
268
		$form->unsetValidator();
269
270
		$form
271
			->addExtraClass('cms-versions-form') // placeholder, necessary for $.metadata() to work
272
			->setAttribute('data-link-tmpl-compare', Controller::join_links($this->Link('compare'), '%s', '%s', '%s'))
273
			->setAttribute('data-link-tmpl-show', Controller::join_links($this->Link('show'), '%s', '%s'));
274
275
		return $form;
276
	}
277
278
	/**
279
	 * Process the {@link VersionsForm} compare function between two pages.
280
	 *
281
	 * @param array
282
	 * @param Form
283
	 *
284
	 * @return html
285
	 */
286
	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...
287
		$versions = $data['Versions'];
288
		if(count($versions) < 2) return null;
289
290
		$id = $this->currentPageID();
0 ignored issues
show
Unused Code introduced by
$id 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...
291
		$version1 = array_shift($versions);
292
		$version2 = array_shift($versions);
293
294
		$form = $this->CompareVersionsForm($version1, $version2);
295
296
		// javascript solution, render into template
297 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...
298
			return $this->customise(array(
299
				"EditForm" => $form
300
			))->renderWith(array(
301
				$this->class . '_EditForm',
302
				'LeftAndMain_Content'
303
			));
304
		}
305
306
		// non javascript, redirect the user to the page
307
		$this->redirect(Controller::join_links(
308
			$this->Link('compare'),
309
			$version1,
310
			$version2
311
		));
312
	}
313
314
	/**
315
	 * Process the {@link VersionsForm} show version function. Only requires
316
	 * one page to be selected.
317
	 *
318
	 * @param array
319
	 * @param Form
320
	 *
321
	 * @return html
322
	 */
323
	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...
324
		$versionID = null;
325
326
		if(isset($data['Versions']) && is_array($data['Versions'])) {
327
			$versionID  = array_shift($data['Versions']);
328
		}
329
330
		if(!$versionID) return;
331
332 View Code Duplication
		if($request->isAjax()) {
0 ignored issues
show
Bug introduced by
The variable $request does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
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...
333
			return $this->customise(array(
334
				"EditForm" => $this->ShowVersionForm($versionID)
335
			))->renderWith(array(
336
				$this->class . '_EditForm',
337
				'LeftAndMain_Content'
338
			));
339
		}
340
341
		// non javascript, redirect the user to the page
342
		$this->redirect(Controller::join_links(
343
			$this->Link('version'),
344
			$versionID
345
		));
346
	}
347
348
	/**
349
	 * @param int|null $versionID
350
	 * @return Form
351
	 */
352
	public function ShowVersionForm($versionID = null) {
353
		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...
354
355
		$id = $this->currentPageID();
356
		$form = $this->getEditForm($id, null, $versionID);
357
358
		return $form;
359
	}
360
361
	/**
362
	 * @param int $versionID
363
	 * @param int $otherVersionID
364
	 * @return Form
365
	 */
366
	public function CompareVersionsForm($versionID, $otherVersionID) {
367
		if($versionID > $otherVersionID) {
368
			$toVersion = $versionID;
369
			$fromVersion = $otherVersionID;
370
		} else {
371
			$toVersion = $otherVersionID;
372
			$fromVersion = $versionID;
373
		}
374
375
		if(!$toVersion || !$fromVersion) return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by CMSPageHistoryController::CompareVersionsForm of type Form|null.

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...
376
377
		$id = $this->currentPageID();
378
		$page = DataObject::get_by_id("SiteTree", $id);
379
380
		if($page && !$page->canView()) {
381
			return Security::permissionFailure($this);
0 ignored issues
show
Bug Compatibility introduced by
The expression \Security::permissionFailure($this); of type SS_HTTPResponse|null adds the type SS_HTTPResponse to the return on line 381 which is incompatible with the return type documented by CMSPageHistoryController::CompareVersionsForm of type Form|null.
Loading history...
382
		}
383
384
		$record = $page->compareVersions($fromVersion, $toVersion);
385
386
		$fromVersionRecord = Versioned::get_version('SiteTree', $id, $fromVersion);
387
		$toVersionRecord = Versioned::get_version('SiteTree', $id, $toVersion);
388
389
		if(!$fromVersionRecord) {
390
			user_error("Can't find version $fromVersion of page $id", E_USER_ERROR);
391
		}
392
393
		if(!$toVersionRecord) {
394
			user_error("Can't find version $toVersion of page $id", E_USER_ERROR);
395
		}
396
397
		if($record) {
398
			$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...
399
			$form->setActions(new FieldList());
400
			$form->addExtraClass('compare');
401
402
			// Comparison views shouldn't be editable.
403
			// Its important to convert fields *before* loading data,
404
			// as the comparison output is HTML and not valid values for the various field types
405
			$readonlyFields = $form->Fields()->makeReadonly();
406
			$form->setFields($readonlyFields);
407
408
			$form->loadDataFrom($record);
409
			$form->loadDataFrom(array(
410
				"ID" => $id,
411
				"Version" => $fromVersion,
412
			));
413
414
			foreach($form->Fields()->dataFields() as $field) {
415
				$field->dontEscape = true;
416
			}
417
418
			return $form;
419
		}
420
	}
421
422
	public function Breadcrumbs($unlinked = false) {
423
		$crumbs = parent::Breadcrumbs($unlinked);
424
		$crumbs[0]->Title = _t('CMSPagesController.MENUTITLE');
425
		return $crumbs;
426
	}
427
428
}
429