Passed
Push — development ( 49962e...4b352c )
by Spuds
01:07 queued 23s
created

SiteDispatcher::checkIfControllerExists()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 26
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
cc 7
eloc 10
nc 4
nop 0
dl 0
loc 26
ccs 0
cts 8
cp 0
crap 56
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Primary site dispatch controller sends the request to the function or method
5
 * registered to handle it.
6
 *
7
 * @package   ElkArte Forum
8
 * @copyright ElkArte Forum contributors
9
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
10
 *
11
 * @version 2.0 Beta 1
12
 *
13
 */
14
15
namespace ElkArte;
16
17
use ElkArte\AdminController\Admin;
18
use ElkArte\AdminController\AdminDebug;
19
use ElkArte\AdminController\ManageThemes;
20
use ElkArte\Controller\Attachment;
21
use ElkArte\Controller\Auth;
22
use ElkArte\Controller\BoardIndex;
23
use ElkArte\Controller\Display;
24
use ElkArte\Controller\Emailmoderator;
25
use ElkArte\Controller\Help;
26
use ElkArte\Controller\Members;
27
use ElkArte\Controller\MergeTopics;
28
use ElkArte\Controller\MessageIndex;
29
use ElkArte\Controller\ModerateAttachments;
30
use ElkArte\Controller\ModerationCenter;
31
use ElkArte\Controller\MoveTopic;
32
use ElkArte\Controller\News;
33
use ElkArte\Controller\Notify;
34
use ElkArte\PersonalMessage\PersonalMessage;
35
use ElkArte\Controller\Poll;
36
use ElkArte\Controller\Post;
37
use ElkArte\Controller\RemoveTopic;
38
use ElkArte\Controller\SplitTopics;
39
use ElkArte\Controller\Unread;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, ElkArte\Unread. Consider defining an alias.

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...
40
use ElkArte\Controller\Xml;
41
use ElkArte\Helper\HttpReq;
42
use ElkArte\Helper\ValuesContainer;
43
use ElkArte\Profile\Profile;
44
use ElkArte\Profile\ProfileHistory;
45
use ElkArte\Profile\ProfileInfo;
46
47
/**
48
 * Dispatch the request to the function or method registered to handle it.
49
 *
50
 * What it does:
51
 *
52
 * - Try first the critical functionality (maintenance, no guest access)
53
 * - Then, in order:
54
 *     * forum's main actions: board index, message index, display topic
55
 *       the current/legacy file/functions registered by ElkArte core
56
 * - Fall back to naming patterns:
57
 *     * filename=[action].php function=[sa]
58
 *     * filename=[action].controller.php method=action_[sa]
59
 *     * filename=[action]-Controller.php method=action_[sa]
60
 * - An addon files to handle custom actions will be called if they follow any of these patterns.
61
 */
62
class SiteDispatcher
63
{
64
	/** @var string Function or method to call */
65
	protected $_function_name;
66
67
	/** @var string Class name for object-oriented controllers */
68
	protected $_controller_name;
69
70
	/** @var AbstractController The instance of the controller */
71
	protected $_controller;
72
73
	/** @var string */
74
	protected $action;
75
76
	/** @var string */
77
	protected $area;
78
79
	/** @var string */
80
	protected $subAction;
81
82
	/** The default action data (controller and function). Every time we don't know what to do, we'll do this :P */
83
	protected $_default_action = [
84
		'controller' => BoardIndex::class,
85
		'function' => 'action_boardindex'
86
	];
87
88
	/** @var string[] Build our nice and cozy err... *cough* */
89
	protected $actionArray = [
90
		'admin' => [Admin::class, 'action_index'],
91
		'attachapprove' => [ModerateAttachments::class, 'action_attachapprove'],
92
		'buddy' => [Members::class, 'action_buddy'],
93
		'collapse' => [BoardIndex::class, 'action_collapse'],
94
		'deletemsg' => [RemoveTopic::class, 'action_deletemsg'],
95
		'dlattach' => [Attachment::class, 'action_index'],
96
		'unwatchtopic' => [Notify::class, 'action_unwatchtopic'],
97
		'editpoll' => [Poll::class, 'action_editpoll'],
98
		'editpoll2' => [Poll::class, 'action_editpoll2'],
99
		'forum' => [BoardIndex::class, 'action_index'],
100
		'quickhelp' => [Help::class, 'action_quickhelp'],
101
		'jsmodify' => [Post::class, 'action_jsmodify'],
102
		'jsoption' => [ManageThemes::class, 'action_jsoption'],
103
		'keepalive' => [Auth::class, 'action_keepalive'],
104
		'lockvoting' => [Poll::class, 'action_lockvoting'],
105
		'login' => [Auth::class, 'action_login'],
106
		'login2' => [Auth::class, 'action_login2'],
107
		'logout' => [Auth::class, 'action_logout'],
108
		'mergetopics' => [MergeTopics::class, 'action_index'],
109
		'moderate' => [ModerationCenter::class, 'action_index'],
110
		'movetopic' => [MoveTopic::class, 'action_movetopic'],
111
		'movetopic2' => [MoveTopic::class, 'action_movetopic2'],
112
		'notifyboard' => [Notify::class, 'action_notifyboard'],
113
		'pm' => [PersonalMessage::class, 'action_index'],
114
		'post2' => [Post::class, 'action_post2'],
115
		'profile' => [Profile::class, 'action_index'],
116
		'profileinfo' => [ProfileInfo::class, 'action_index'],
117
		'quotefast' => [Post::class, 'action_quotefast'],
118
		'quickmod' => [MessageIndex::class, 'action_quickmod'],
119
		'quickmod2' => [Display::class, 'action_quickmod2'],
120
		'removetopic2' => [RemoveTopic::class, 'action_removetopic2'],
121
		'reporttm' => [Emailmoderator::class, 'action_reporttm'],
122
		'restoretopic' => [RemoveTopic::class, 'action_restoretopic'],
123
		'splittopics' => [SplitTopics::class, 'action_splittopics'],
124
		'trackip' => [ProfileHistory::class, 'action_trackip'],
125
		'unreadreplies' => [Unread::class, 'action_unreadreplies'],
126
		'viewprofile' => [Profile::class, 'action_index'],
127
		'viewquery' => [AdminDebug::class, 'action_viewquery'],
128
		'.xml' => [News::class, 'action_showfeed'],
129
		'xmlhttp' => [Xml::class, 'action_index'],
130
	];
131
132
	/**
133
	 * Create an instance and initialize it.
134
	 *
135
	 * This does all the work to figure out which controller and method need
136
	 * to be called.
137
	 *
138
	 * @param HttpReq $_req
139
	 */
140
	public function __construct(HttpReq $_req)
141
	{
142 4
		global $context;
143
144 4
		$context['current_action'] = $this->action = $_req->getQuery('action', 'trim|strval', '');
145
		$this->area = $_req->getQuery('area', 'trim|strval', '');
146 4
		$context['current_subaction'] = $this->subAction = $_req->getQuery('sa', 'trim|strval', '');
147 4
		$this->_default_action = $this->getFrontPage();
148 4
		$this->determineDefaultAction();
149 4
150 4
		if (empty($this->_controller_name))
151
		{
152 4
			$this->determineAction();
153
		}
154 4
155
		// Initialize this controller with its event manager
156
		$this->_controller = new $this->_controller_name(new EventManager());
157
	}
158 4
159 4
	/**
160
	 * Retrieves the front page action based on the mod settings
161
	 *
162
	 * @return array The default action for the front page
163
	 */
164 4
	protected function getFrontPage(): array
165
	{
166 4
		global $modSettings;
167
168
		if (!empty($modSettings['front_page'])
169 4
			&& class_exists($modSettings['front_page'])
170 4
			&& in_array('frontPageHook', get_class_methods($modSettings['front_page'])))
171
		{
172
			$modSettings['default_forum_action'] = ['action' => 'forum'];
173
			call_user_func_array([$modSettings['front_page'], 'frontPageHook'], [&$this->_default_action]);
174
		}
175
		else
176
		{
177
			$modSettings['default_forum_action'] = [];
178 4
		}
179
180
		return $this->_default_action;
181 4
	}
182
183
	/**
184
	 * Finds out if the current action is one of those without an "action" parameter in the URL
185
	 */
186
	protected function determineDefaultAction(): void
187
	{
188 4
		global $board, $topic;
189
190 4
		if (empty($this->action))
191
		{
192 4
			// Home page: board index
193
			if (empty($board) && empty($topic))
194
			{
195 2
				// Was it, wasn't it...
196
				if (empty($this->_function_name))
197
				{
198 2
					$this->_controller_name = $this->_default_action['controller'];
199
					$this->_function_name = $this->_default_action['function'];
200 2
				}
201 2
			}
202
			// ?board=b message index
203
			elseif (empty($topic))
204
			{
205
				$this->_controller_name = MessageIndex::class;
206
				$this->_function_name = 'action_messageindex';
207 2
			}
208 2
			// board=b;topic=t topic display
209
			else
210
			{
211
				$this->_controller_name = Display::class;
212
				$this->_function_name = 'action_display';
213 2
			}
214 2
		}
215
	}
216
217 4
	/**
218
	 * Determines the controller and function names based on the current action.
219
	 * Allows extending or changing the action array through a hook.
220
	 * If the controller class does not exist, sets the default controller and function names.
221
	 */
222
	protected function determineAction(): void
223
	{
224
		// Allow extending or changing $actionArray through a hook
225
		// Format: $_GET['action'] => array($class, $method)
226 4
		call_integration_hook('integrate_actions', [&$this->actionArray]);
227
228
		// Is it in the action list?
229
		if (isset($this->actionArray[$this->action]))
230
		{
231 4
			$this->setActionAndControllerFromActionArray();
232
		}
233
		// Fall back to naming patterns, Addons can use any of them, and it should Just Work (tm).
234 4
		elseif (preg_match('~^[a-zA-Z_\\-]+\d*$~', $this->action))
235
		{
236 4
			$this->setActionAndControllerFromNamingPatterns();
237
		}
238
239 4
		// The file and function weren't found yet?  Then set a default!
240
		$this->setDefaultActionAndControllerIfEmpty();
241 4
242
		$this->handleApiCall();
243
244
		// Ensure both the controller and action exist and are callable
245
		if ($this->checkIfControllerExists())
246
		{
247
			return;
248
		}
249
250 4
		// This should never happen, that's why it's here :P
251
		$this->setDefaultActionAndController();
252
	}
253
254
	/**
255 2
	 * If the current action is in the action array, sets the controller and function names
256
	 * based on the action array value.
257
	 *
258
	 * What it does:
259 2
	 *   - If the method is specified in the action array, uses it as the function name.
260 2
	 *   - If the subAction is specified and matches the pattern, appends it to "action_" as the function name.
261
	 *   - If none of the above conditions are met, sets the function name to "action_index".
262 2
	 */
263
	protected function setActionAndControllerFromActionArray(): void
264
	{
265
		$this->_controller_name = $this->actionArray[$this->action][0];
266 2
		$this->_function_name = 'action_index';
267
268
		// If the method is coded in, use it
269
		if (!empty($this->actionArray[$this->action][1]))
270
		{
271 4
			$this->_function_name = $this->actionArray[$this->action][1];
272
		}
273
		// Otherwise fall back to naming patterns
274
		elseif (!empty($this->subAction) && preg_match('~^\w+$~', $this->subAction))
275
		{
276
			$this->_function_name = 'action_' . $this->subAction;
277
		}
278 4
	}
279
280
	/**
281
	 * Sets the action and controller names based on naming patterns.
282
	 *
283
	 * What it does:
284 4
	 * - The action name is used to determine the controller class, action=gallery => Gallery.php
285
	 * - The subAction is used to determine the action function, sa=upload => action_upload()
286 4
	 * - If the subAction is not set or the area is set, the action function will default to 'action_index'
287
	 * - controller classes must be in the Controller directory or in the Addons directory
288 2
	 * - action functions must be:
289
	 *      - In the Controller directory
290 2
	 *      - Or in the Addons directory as a subdirectory named the same as the action
291
	 */
292
	protected function setActionAndControllerFromNamingPatterns(): void
293
	{
294
		// Try the main ElkArte controller directory first
295 2
		$this->_controller_name = '\\ElkArte\\Controller\\' . ucfirst($this->action);
296 2
297
		if (!class_exists($this->_controller_name))
298 2
		{
299
			// Try the Addons directory
300
			$this->_controller_name = '\\Addons\\' . ucfirst($this->action) . '\\' . ucfirst($this->action);
301
		}
302
303
		// Now try to find the action function
304
		if ($this->subAction !== null && empty($this->area) && preg_match('~^\w+$~', $this->subAction))
305
		{
306
			$this->_function_name = 'action_' . $this->subAction;
307
		}
308
		else
309
		{
310
			$this->_function_name = 'action_index';
311
		}
312
	}
313
314
	/**
315
	 * Sets the default action and controller if they are empty.
316
	 * If either the controller or the function name is empty, this method calls the setDefaultActionAndController method.
317
	 */
318
	protected function setDefaultActionAndControllerIfEmpty(): void
319
	{
320
		if (empty($this->_controller_name) || empty($this->_function_name))
321
		{
322
			$this->setDefaultActionAndController();
323
		}
324
	}
325
326
	/**
327
	 * Sets the default action and controller names.
328
	 *
329
	 * What it does:
330
	 *  - This method is used to set the default action and controller names
331
	 *  - When the current action does not have an "action" parameter in the URL.
332
	 *  - It assigns the values from the `_default_action` property to the `_controller_name` and `_function_name` properties.
333
	 */
334
	protected function setDefaultActionAndController(): void
335
	{
336
		$this->_controller_name = $this->_default_action['controller'];
337
		$this->_function_name = $this->_default_action['function'];
338
	}
339
340
	/**
341
	 * Determines if the current API call should be handled separately.
342
	 *
343
	 * What it does:
344
	 *  - If the 'api' parameter is set in the request and its value is empty, it appends
345
	 * the '_api' suffix to the current function name.
346
	 *  - This needs to be reviewed; all api calls really should be qualified as
347
	 * JSON, XML, HTML, etc.
348
	 */
349
	protected function handleApiCall(): void
350
	{
351
		if (!isset($_REQUEST['api']))
352
		{
353
			return;
354
		}
355
356
		if ($_REQUEST['api'] !== '')
357
		{
358
			return;
359
		}
360
361
		$this->_function_name .= '_api';
362
	}
363
364
	/**
365
	 * Checks if the specified controller and its requested method exist and can be called.
366
	 *
367
	 * What it does:
368
	 *  - The method verifies whether the controller's class exists and determines if the
369
	 * specified function is callable within that class.
370
	 *  - If the requested sa is not "action_index" and the controller is an AbstractController,
371
	 * the method will revert to "action_index" so the sa is dispatched by the class to
372
	 * ensure permissions etc. are checked.
373
	 *
374
	 * @return bool Returns true if the controller and method exist and are callable, otherwise false.
375
	 */
376
	protected function checkIfControllerExists(): bool
377
	{
378
		// 3, 2, ... and go
379
		if (class_exists($this->_controller_name))
380
		{
381
			// Maybe the default requires an abstract method
382
			if ($this->_function_name !== 'action_index'
383
				&& !isset($this->actionArray[$this->action])
384
				&& in_array('action_index', get_class_methods($this->_controller_name), true)
385
				&& is_subclass_of($this->_controller_name, AbstractController::class))
386
			{
387
				// Calling a sa directly on an abstract class? This should be dispatched by the
388
				// class itself ($action->dispatch($subAction) to ensure permissions
389
				// etc. are checked.
390
				$this->_function_name = 'action_index';
391
				return true;
392
			}
393
394
			// Method requested is in the list of its callable methods
395
			if (in_array($this->_function_name, get_class_methods($this->_controller_name), true))
396
			{
397
				return true;
398
			}
399
		}
400
401
		return false;
402
	}
403
404
	/**
405
	 * Passes the \ElkArte\User::$info variable to the controller
406
	 *
407
	 * @param ValuesContainer $user
408
	 */
409
	public function setUser($user): void
410
	{
411
		$this->_controller->setUser($user);
412
	}
413
414
	/**
415
	 * Relay control to the respective function or method.
416
	 *
417
	 * What it does:
418
	 *
419
	 * - Calls a generic pre (_before) integration hook based on the controllers class name.
420 2
	 *   - e.g., integrate_action_draft_before will be called before \ElkArte\Controller\Draft
421
	 * - Calls the controllers pre_dispatch method, provides increased flexibility over simple _constructor
422 2
	 * - Calls the controllers selected method
423
	 * - Calls generic post (_after) integration hook based on the controllers class name.
424
	 *   - e.g., integrate_action_draft_after will be called after \ElkArte\Controller\Draft assuming it returns
425
	 * normally from the controller (e.g., no fatal error, no redirect)
426
	 *
427
	 * @event integrate_action_xyz_before
428
	 * @event integrate_action_xyz_after
429
	 */
430
	public function dispatch()
431
	{
432
		// Fetch controllers generic hook name from the action controller
433
		$hook = $this->_controller->getHook();
434
435
		// Call the controllers pre-dispatch method
436
		$this->_controller->pre_dispatch();
437
438
		// Call integrate_action_XYZ_before then XYZ_controller_>123 then integrate_action_XYZ_after
439
		call_integration_hook('integrate_action_' . $hook . '_before', [$this->_function_name]);
440
441
		$result = $this->_controller->{$this->_function_name}();
442
443
		// Remember kids, if your controller bails, you will not get here
444
		call_integration_hook('integrate_action_' . $hook . '_after', [$this->_function_name]);
445
446
		return $result;
447
	}
448
449
	/**
450
	 * If the current controller needs to load all the security framework.
451
	 */
452
	public function needSecurity(): bool
453
	{
454
		return $this->_controller->needSecurity($this->_function_name);
455
	}
456
457
	/**
458
	 * If the current controller needs to load the theme.
459
	 */
460
	public function needTheme(): bool
461
	{
462
		global $maintenance;
463
464
		// Maintenance mode: you're out of here unless you're admin
465
		if (!empty($maintenance) && !allowedTo('admin_forum'))
466
		{
467
			// You can only log in
468
			if ($this->action === 'login2' || $this->action === 'logout')
469
			{
470
				$this->_controller_name = Auth::class;
471
				$this->_function_name = 'action_' . $this->action;
472
			}
473
			// "maintenance mode" page
474
			else
475
			{
476
				$this->_controller_name = Auth::class;
477
				$this->_function_name = 'action_maintenance_mode';
478
			}
479
480
			// re-initialize the controller and the event manager
481
			$this->_controller = new $this->_controller_name(new EventManager());
482
		}
483
		// If guest access is disallowed, a guest is kicked out... politely. :P
484
		elseif ($this->restrictedGuestAccess())
485
		{
486
			$this->_controller_name = Auth::class;
487
			$this->_function_name = 'action_kickguest';
488
489
			// re-initialize... you got the drift
490
			$this->_controller = new $this->_controller_name(new EventManager());
491
		}
492
493
		return $this->_controller->needTheme($this->_function_name);
494
	}
495
496
	/**
497
	 * Determine if guest access is restricted, and, if so,
498
	 * only allow the listed actions
499
	 *
500
	 * @return bool
501
	 */
502
	protected function restrictedGuestAccess(): bool
503
	{
504
		global $modSettings;
505
506
		return empty($modSettings['allow_guestAccess'])
507
			&& User::$info->is_guest
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
508
			&& !in_array($this->action, ['login', 'login2', 'register', 'reminder', 'help', 'quickhelp', 'mailq']);
509
	}
510
511
	/**
512
	 * If the current controller wants to track access and stats.
513
	 */
514
	public function trackStats($action = ''): bool
515
	{
516
		return $this->_controller->trackStats($this->_function_name);
517
	}
518
519
	/**
520
	 * @return AbstractController
521
	 */
522
	public function getController(): AbstractController
523
	{
524
		return $this->_controller;
525
	}
526
527
	/**
528
	 * Returns the current action for the system
529
	 *
530
	 * @return string
531
	 */
532
	public function site_action(): string
533
	{
534
		if (!empty($this->_controller_name))
535
		{
536
			$action = strtolower(ltrim(strrchr($this->_controller_name, "\\"), "\\"));
537
			$action = str_ends_with($action, "2") ? substr($action, 0, -1) : $action;
538
		}
539
540
		return $action ?? $this->action;
541
	}
542
}
543