Completed
Pull Request — master (#1458)
by Sam
04:45 queued 02:13
created

ContentController   D

Complexity

Total Complexity 83

Size/Duplication

Total Lines 432
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 3
Bugs 1 Features 0
Metric Value
wmc 83
lcom 1
cbo 4
dl 0
loc 432
rs 4.8717
c 3
b 1
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 3
A Link() 0 3 2
A ChildrenOf() 0 9 4
A Page() 0 3 1
D init() 0 30 17
C handleRequest() 0 58 14
A project() 0 4 1
A data() 0 3 1
C getMenu() 0 34 8
A Menu() 0 3 1
A LoginForm() 0 3 1
A SiteConfig() 0 7 2
B ContentLocale() 0 11 5
C getViewer() 0 25 7
B successfullyinstalled() 0 24 4
B deleteinstallfiles() 0 39 5
B SilverStripeNavigator() 0 56 7

How to fix   Complexity   

Complex Class

Complex classes like ContentController 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 ContentController, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
use SilverStripe\Model\FieldType\DBVarchar;
4
use SilverStripe\Model\FieldType\DBHTMLText;
5
6
/**
7
 * The most common kind of controller; effectively a controller linked to a {@link DataObject}.
8
 *
9
 * ContentControllers are most useful in the content-focused areas of a site.  This is generally
10
 * the bulk of a site; however, they may be less appropriate in, for example, the user management
11
 * section of an application.
12
 *
13
 * On its own, content controller does very little.  Its constructor is passed a {@link DataObject}
14
 * which is stored in $this->dataRecord.  Any unrecognised method calls, for example, Title()
15
 * and Content(), will be passed along to the data record,
16
 *
17
 * Subclasses of ContentController are generally instantiated by ModelAsController; this will create
18
 * a controller based on the URLSegment action variable, by looking in the SiteTree table.
19
 *
20
 * @todo Can this be used for anything other than SiteTree controllers?
21
 *
22
 * @package cms
23
 * @subpackage control
24
 */
25
class ContentController extends Controller {
26
27
	protected $dataRecord;
28
29
	private static $extensions = array('OldPageRedirector');
30
31
	private static $allowed_actions = array(
32
		'successfullyinstalled',
33
		'deleteinstallfiles', // secured through custom code
34
		'LoginForm'
35
	);
36
37
	/**
38
	 * The ContentController will take the URLSegment parameter from the URL and use that to look
39
	 * up a SiteTree record.
40
	 */
41
	public function __construct($dataRecord = null) {
42
		if(!$dataRecord) {
43
			$dataRecord = new Page();
44
			if($this->hasMethod("Title")) $dataRecord->Title = $this->Title();
45
			$dataRecord->URLSegment = get_class($this);
46
			$dataRecord->ID = -1;
47
		}
48
49
		$this->dataRecord = $dataRecord;
50
51
		parent::__construct();
52
53
		$this->setFailover($this->dataRecord);
54
	}
55
56
	/**
57
	 * Return the link to this controller, but force the expanded link to be returned so that form methods and
58
	 * similar will function properly.
59
	 *
60
	 * @param string|null $action Action to link to.
61
	 * @return string
62
	 */
63
	public function Link($action = null) {
64
		return $this->data()->Link(($action ? $action : true));
65
	}
66
67
	//----------------------------------------------------------------------------------//
68
	// These flexible data methods remove the need for custom code to do simple stuff
69
70
	/**
71
	 * Return the children of a given page. The parent reference can either be a page link or an ID.
72
	 *
73
	 * @param string|int $parentRef
74
	 * @return SS_List
75
	 */
76
	public function ChildrenOf($parentRef) {
77
		$parent = SiteTree::get_by_link($parentRef);
78
79
		if(!$parent && is_numeric($parentRef)) {
80
			$parent = DataObject::get_by_id('SiteTree', $parentRef);
81
		}
82
83
		if($parent) return $parent->Children();
84
	}
85
86
	/**
87
	 * @param string $link
88
	 * @return SiteTree
89
	 */
90
	public function Page($link) {
91
		return SiteTree::get_by_link($link);
92
	}
93
94
	public function init() {
95
		parent::init();
96
97
		// If we've accessed the homepage as /home/, then we should redirect to /.
98
		if($this->dataRecord && $this->dataRecord instanceof SiteTree
99
			 	&& RootURLController::should_be_on_root($this->dataRecord) && (!isset($this->urlParams['Action']) || !$this->urlParams['Action'] )
100
				&& !$_POST && !$_FILES && !$this->redirectedTo() ) {
101
			$getVars = $_GET;
102
			unset($getVars['url']);
103
			if($getVars) $url = "?" . http_build_query($getVars);
104
			else $url = "";
105
			$this->redirect($url, 301);
106
			return;
107
		}
108
109
		if($this->dataRecord) $this->dataRecord->extend('contentcontrollerInit', $this);
110
		else singleton('SiteTree')->extend('contentcontrollerInit', $this);
111
112
		if($this->redirectedTo()) return;
113
114
		// Check page permissions
115
		if($this->dataRecord && $this->URLSegment != 'Security' && !$this->dataRecord->canView()) {
116
			return Security::permissionFailure($this);
117
		}
118
119
		// Use theme from the site config
120
		if(($config = SiteConfig::current_site_config()) && $config->Theme) {
121
			Config::inst()->update('SSViewer', 'theme', $config->Theme);
122
		}
123
	}
124
125
	/**
126
	 * This acts the same as {@link Controller::handleRequest()}, but if an action cannot be found this will attempt to
127
	 * fall over to a child controller in order to provide functionality for nested URLs.
128
	 *
129
	 * @param SS_HTTPRequest $request
130
	 * @param DataModel $model
131
	 * @return SS_HTTPResponse
132
	 * @throws SS_HTTPResponse_Exception
133
	 */
134
	public function handleRequest(SS_HTTPRequest $request, DataModel $model = null) {
135
		$child  = null;
136
		$action = $request->param('Action');
137
		$this->setDataModel($model);
138
139
		// If nested URLs are enabled, and there is no action handler for the current request then attempt to pass
140
		// control to a child controller. This allows for the creation of chains of controllers which correspond to a
141
		// nested URL.
142
		if($action && SiteTree::config()->nested_urls && !$this->hasAction($action)) {
143
			// See ModelAdController->getNestedController() for similar logic
144
			if(class_exists('Translatable')) Translatable::disable_locale_filter();
145
			// look for a page with this URLSegment
146
			$child = $this->model->SiteTree->filter(array(
147
				'ParentID' => $this->ID,
148
				'URLSegment' => rawurlencode($action)
149
			))->first();
150
			if(class_exists('Translatable')) Translatable::enable_locale_filter();
151
		}
152
153
		// we found a page with this URLSegment.
154
		if($child) {
155
			$request->shiftAllParams();
156
			$request->shift();
157
158
			$response = ModelAsController::controller_for($child)->handleRequest($request, $model);
159
		} else {
160
			// If a specific locale is requested, and it doesn't match the page found by URLSegment,
161
			// look for a translation and redirect (see #5001). Only happens on the last child in
162
			// a potentially nested URL chain.
163
			if(class_exists('Translatable')) {
164
				$locale = $request->getVar('locale');
165
				if($locale && i18n::validate_locale($locale) && $this->dataRecord && $this->dataRecord->Locale != $locale) {
166
					$translation = $this->dataRecord->getTranslation($locale);
167
					if($translation) {
168
						$response = new SS_HTTPResponse();
169
						$response->redirect($translation->Link(), 301);
170
						throw new SS_HTTPResponse_Exception($response);
171
					}
172
				}
173
			}
174
175
			Director::set_current_page($this->data());
176
177
			try {
178
				$response = parent::handleRequest($request, $model);
179
180
				Director::set_current_page(null);
181
			} catch(SS_HTTPResponse_Exception $e) {
0 ignored issues
show
Bug introduced by
The class SS_HTTPResponse_Exception does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
182
				$this->popCurrent();
183
184
				Director::set_current_page(null);
185
186
				throw $e;
187
			}
188
		}
189
190
		return $response;
191
	}
192
193
	/**
194
	 * Get the project name
195
	 *
196
	 * @return string
197
	 */
198
	public function project() {
199
		global $project;
200
		return $project;
201
	}
202
203
	/**
204
	 * Returns the associated database record
205
	 */
206
	public function data() {
207
		return $this->dataRecord;
208
	}
209
210
	/*--------------------------------------------------------------------------------*/
211
212
	/**
213
	 * Returns a fixed navigation menu of the given level.
214
	 * @param int $level Menu level to return.
215
	 * @return ArrayList
216
	 */
217
	public function getMenu($level = 1) {
218
		if($level == 1) {
219
			$result = SiteTree::get()->filter(array(
220
				"ShowInMenus" => 1,
221
				"ParentID" => 0
222
			));
223
224
		} else {
225
			$parent = $this->data();
226
			$stack = array($parent);
227
228
			if($parent) {
229
				while($parent = $parent->Parent) {
230
					array_unshift($stack, $parent);
231
				}
232
			}
233
234
			if(isset($stack[$level-2])) $result = $stack[$level-2]->Children();
235
		}
236
237
		$visible = array();
238
239
		// Remove all entries the can not be viewed by the current user
240
		// We might need to create a show in menu permission
241
 		if(isset($result)) {
242
			foreach($result as $page) {
243
				if($page->canView()) {
244
					$visible[] = $page;
245
				}
246
			}
247
		}
248
249
		return new ArrayList($visible);
250
	}
251
252
	public function Menu($level) {
253
		return $this->getMenu($level);
254
	}
255
256
	/**
257
	 * Returns the default log-in form.
258
	 *
259
	 * @todo Check if here should be returned just the default log-in form or
260
	 *       all available log-in forms (also OpenID...)
261
	 */
262
	public function LoginForm() {
263
		return MemberAuthenticator::get_login_form($this);
264
	}
265
266
	public function SilverStripeNavigator() {
267
		$member = Member::currentUser();
268
		$items = '';
269
		$message = '';
270
271
		if(Director::isDev() || Permission::check('CMS_ACCESS_CMSMain') || Permission::check('VIEW_DRAFT_CONTENT')) {
272
			if($this->dataRecord) {
273
				Requirements::css(CMS_DIR . '/client/dist/styles/SilverStripeNavigator.css');
274
				Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
275
				Requirements::javascript(CMS_DIR . '/client/dist/js/SilverStripeNavigator.js');
276
277
				$return = $nav = SilverStripeNavigator::get_for_record($this->dataRecord);
0 ignored issues
show
Unused Code introduced by
$nav 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...
278
				$items = $return['items'];
279
				$message = $return['message'];
280
			}
281
282
			if($member) {
283
				$firstname = Convert::raw2xml($member->FirstName);
284
				$surname = Convert::raw2xml($member->Surname);
285
				$logInMessage = _t('ContentController.LOGGEDINAS', 'Logged in as') ." {$firstname} {$surname} - <a href=\"Security/logout\">". _t('ContentController.LOGOUT', 'Log out'). "</a>";
286
			} else {
287
				$logInMessage = sprintf(
288
					'%s - <a href="%s">%s</a>' ,
289
					_t('ContentController.NOTLOGGEDIN', 'Not logged in') ,
290
					Config::inst()->get('Security', 'login_url'),
291
					_t('ContentController.LOGIN', 'Login') ."</a>"
292
				);
293
			}
294
			$viewPageIn = _t('ContentController.VIEWPAGEIN', 'View Page in:');
295
296
			return <<<HTML
297
				<div id="SilverStripeNavigator">
298
					<div class="holder">
299
					<div id="logInStatus">
300
						$logInMessage
301
					</div>
302
303
					<div id="switchView" class="bottomTabs">
304
						$viewPageIn
305
						$items
306
					</div>
307
					</div>
308
				</div>
309
					$message
310
HTML;
311
312
		// On live sites we should still see the archived message
313
		} else {
314
			if($date = Versioned::current_archived_date()) {
315
				Requirements::css(CMS_DIR . '/client/dist/styles/SilverStripeNavigator.css');
316
				$dateObj = Datetime::create($date, null);
0 ignored issues
show
Bug introduced by
The method create() does not exist on DateTime. Did you maybe mean createFromFormat()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
317
				// $dateObj->setVal($date);
318
				return "<div id=\"SilverStripeNavigatorMessage\">". _t('ContentController.ARCHIVEDSITEFROM') ."<br>" . $dateObj->Nice() . "</div>";
319
			}
320
		}
321
	}
322
323
	public function SiteConfig() {
324
		if(method_exists($this->dataRecord, 'getSiteConfig')) {
325
			return $this->dataRecord->getSiteConfig();
326
		} else {
327
			return SiteConfig::current_site_config();
328
		}
329
	}
330
331
	/**
332
	 * Returns an RFC1766 compliant locale string, e.g. 'fr-CA'.
333
	 * Inspects the associated {@link dataRecord} for a {@link SiteTree->Locale} value if present,
334
	 * and falls back to {@link Translatable::get_current_locale()} or {@link i18n::default_locale()},
335
	 * depending if Translatable is enabled.
336
	 *
337
	 * Suitable for insertion into lang= and xml:lang=
338
	 * attributes in HTML or XHTML output.
339
	 *
340
	 * @return string
341
	 */
342
	public function ContentLocale() {
343
		if($this->dataRecord && $this->dataRecord->hasExtension('Translatable')) {
344
			$locale = $this->dataRecord->Locale;
345
		} elseif(class_exists('Translatable') && SiteTree::has_extension('Translatable')) {
346
			$locale = Translatable::get_current_locale();
347
		} else {
348
			$locale = i18n::get_locale();
349
		}
350
351
		return i18n::convert_rfc1766($locale);
352
	}
353
354
355
	/**
356
	 * Return an SSViewer object to render the template for the current page.
357
	 *
358
	 * @param $action string
359
	 *
360
	 * @return SSViewer
361
	 */
362
	public function getViewer($action) {
363
		// Manually set templates should be dealt with by Controller::getViewer()
364
		if(isset($this->templates[$action]) && $this->templates[$action]
365
			|| (isset($this->templates['index']) && $this->templates['index'])
366
			|| $this->template
367
		) {
368
			return parent::getViewer($action);
369
		}
370
371
		// Prepare action for template search
372
		if($action == "index") $action = "";
373
		else $action = '_' . $action;
374
375
		$templates = array_merge(
376
			// Find templates by dataRecord
377
			SSViewer::get_templates_by_class(get_class($this->dataRecord), $action, "SiteTree"),
378
			// Next, we need to add templates for all controllers
379
			SSViewer::get_templates_by_class(get_class($this), $action, "Controller"),
380
			// Fail-over to the same for the "index" action
381
			SSViewer::get_templates_by_class(get_class($this->dataRecord), "", "SiteTree"),
382
			SSViewer::get_templates_by_class(get_class($this), "", "Controller")
383
		);
384
385
		return new SSViewer($templates);
386
	}
387
388
389
	/**
390
	 * This action is called by the installation system
391
	 */
392
	public function successfullyinstalled() {
393
		// Return 410 Gone if this site is not actually a fresh installation
394
		if (!file_exists(BASE_PATH . '/install.php')) {
395
			$this->httpError(410);
396
		}
397
398
		// TODO Allow this to work when allow_url_fopen=0
399
		if(isset($_SESSION['StatsID']) && $_SESSION['StatsID']) {
400
			$url = 'http://ss2stat.silverstripe.com/Installation/installed?ID=' . $_SESSION['StatsID'];
401
			@file_get_contents($url);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
402
		}
403
404
		global $project;
405
		$data = new ArrayData(array(
406
			'Project' => Convert::raw2xml($project),
407
			'Username' => Convert::raw2xml(Session::get('username')),
408
			'Password' => Convert::raw2xml(Session::get('password')),
409
		));
410
411
		return array(
412
			"Title" =>  _t("ContentController.INSTALL_SUCCESS", "Installation Successful!"),
413
			"Content" => $data->renderWith('Install_successfullyinstalled'),
414
		);
415
	}
416
417
	public function deleteinstallfiles() {
418
		if(!Permission::check("ADMIN")) return Security::permissionFailure($this);
419
420
		$title = new DBVarchar("Title");
421
		$content = new DBHTMLText('Content');
422
423
		// We can't delete index.php as it might be necessary for URL routing without mod_rewrite.
424
		// There's no safe way to detect usage of mod_rewrite across webservers,
425
		// so we have to assume the file is required.
426
		$installfiles = array(
427
			'install.php',
428
			'config-form.css',
429
			'config-form.html',
430
			'index.html'
431
		);
432
433
		$unsuccessful = new ArrayList();
434
		foreach($installfiles as $installfile) {
435
			if(file_exists(BASE_PATH . '/' . $installfile)) {
436
				@unlink(BASE_PATH . '/' . $installfile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
437
			}
438
439
			if(file_exists(BASE_PATH . '/' . $installfile)) {
440
				$unsuccessful->push(new ArrayData(array('File' => $installfile)));
441
			}
442
		}
443
444
		$data = new ArrayData(array(
445
			'Username' => Convert::raw2xml(Session::get('username')),
446
			'Password' => Convert::raw2xml(Session::get('password')),
447
			'UnsuccessfulFiles' => $unsuccessful
448
		));
449
		$content->setValue($data->renderWith('Install_deleteinstallfiles'));
450
451
		return array(
452
			"Title" => $title,
453
			"Content" => $content,
454
		);
455
	}
456
}
457