Passed
Push — master ( d20083...d26eb3 )
by Fabio
05:33
created

TPageService::getDefaultPage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
1
<?php
2
/**
3
 * TPageService class file.
4
 *
5
 * @author Qiang Xue <[email protected]>
6
 * @link https://github.com/pradosoft/prado
7
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
8
 * @package Prado\Web\Services
9
 */
10
11
namespace Prado\Web\Services;
12
13
use Prado\Prado;
14
use Prado\Exceptions\TConfigurationException;
15
use Prado\Exceptions\THttpException;
16
use Prado\Exceptions\TInvalidOperationException;
17
use Prado\TApplication;
18
use Prado\TApplicationMode;
19
use Prado\Web\UI\TPage;
20
use Prado\Web\UI\TTemplateManager;
21
use Prado\Web\UI\TThemeManager;
22
23
/**
24
 * TPageService class.
25
 *
26
 * TPageService implements the service for serving user page requests.
27
 *
28
 * Pages that are available to client users are stored under a directory specified by
29
 * {@link setBasePath BasePath}. The directory may contain subdirectories.
30
 * Pages serving for a similar goal are usually placed under the same directory.
31
 * A directory may contain a configuration file <b>config.xml</b> whose content
32
 * is similar to that of application configuration file.
33
 *
34
 * A page is requested via page path, which is a dot-connected directory names
35
 * appended by the page name. Assume '<BasePath>/Users/Admin' is the directory
36
 * containing the page 'Update'. Then the page can be requested via 'Users.Admin.Update'.
37
 * By default, the {@link setBasePath BasePath} of the page service is the "pages"
38
 * directory under the application base path. You may change this default
39
 * by setting {@link setBasePath BasePath} with a different path you prefer.
40
 *
41
 * Page name refers to the file name (without extension) of the page template.
42
 * In order to differentiate from the common control template files, the extension
43
 * name of the page template files must be '.page'. If there is a PHP file with
44
 * the same page name under the same directory as the template file, that file
45
 * will be considered as the page class file and the file name is the page class name.
46
 * If such a file is not found, the page class is assumed as {@link TPage}.
47
 *
48
 * Modules can be configured and loaded in page directory configurations.
49
 * Configuration of a module in a subdirectory will overwrite its parent
50
 * directory's configuration, if both configurations refer to the same module.
51
 *
52
 * By default, TPageService will automatically load two modules:
53
 * - {@link TTemplateManager} : manages page and control templates
54
 * - {@link TThemeManager} : manages themes used in a Prado application
55
 *
56
 * In page directory configurations, static authorization rules can also be specified,
57
 * which governs who and which roles can access particular pages.
58
 * Refer to {@link TAuthorizationRule} for more details about authorization rules.
59
 * Page authorization rules can be configured within an <authorization> tag in
60
 * each page directory configuration as follows,
61
 * <authorization>
62
 *   <deny pages="Update" users="?" />
63
 *   <allow pages="Admin" roles="administrator" />
64
 *   <deny pages="Admin" users="*" />
65
 * </authorization>
66
 * where the 'pages' attribute may be filled with a sequence of comma-separated
67
 * page IDs. If 'pages' attribute does not appear in a rule, the rule will be
68
 * applied to all pages in this directory and all subdirectories (recursively).
69
 * Application of authorization rules are in a bottom-up fashion, starting from
70
 * the directory containing the requested page up to all parent directories.
71
 * The first matching rule will be used. The last rule always allows all users
72
 * accessing to any resources.
73
 *
74
 * @author Qiang Xue <[email protected]>
75
 * @author Carl G. Mathisen <[email protected]>
76
 * @package Prado\Web\Services
77
 * @since 3.0
78
 */
79
class TPageService extends \Prado\TService
80
{
81
	/**
82
	 * Configuration file name
83
	 */
84
	public const CONFIG_FILE_XML = 'config.xml';
85
	/**
86
	 * Configuration file name
87
	 */
88
	public const CONFIG_FILE_PHP = 'config.php';
89
	/**
90
	 * Default base path
91
	 */
92
	public const DEFAULT_BASEPATH = 'Pages';
93
	/**
94
	 * Fallback base path - used to be the default up to Prado < 3.2
95
	 */
96
	public const FALLBACK_BASEPATH = 'pages';
97
	/**
98
	 * Prefix of ID used for storing parsed configuration in cache
99
	 */
100
	public const CONFIG_CACHE_PREFIX = 'prado:pageservice:';
101
	/**
102
	 * Page template file extension
103
	 */
104
	public const PAGE_FILE_EXT = '.page';
105
	/**
106
	 * Prefix of Pages used for instantiating new pages
107
	 */
108
	public const PAGE_NAMESPACE_PREFIX = 'Application\\Pages\\';
109
	/**
110
	 * @var string root path of pages
111
	 */
112
	private $_basePath;
113
	/**
114
	 * @var string base path class in namespace format
115
	 */
116
	private $_basePageClass = '\Prado\Web\UI\TPage';
117
	/**
118
	 * @var string clientscript manager class in namespace format
119
	 * @since 3.1.7
120
	 */
121
	private $_clientScriptManagerClass = '\Prado\Web\UI\TClientScriptManager';
122
	/**
123
	 * @var string default page
124
	 */
125
	private $_defaultPage = 'Home';
126
	/**
127
	 * @var string requested page (path)
128
	 */
129
	private $_pagePath;
130
	/**
131
	 * @var TPage the requested page
132
	 */
133
	private $_page;
134
	/**
135
	 * @var array list of initial page property values
136
	 */
137
	private $_properties = [];
138
	/**
139
	 * @var bool whether service is initialized
140
	 */
141
	private $_initialized = false;
142
	/**
143
	 * @var TThemeManager theme manager
144
	 */
145
	private $_themeManager;
146
	/**
147
	 * @var TTemplateManager template manager
148
	 */
149
	private $_templateManager;
150
151
	/**
152
	 * Initializes the service.
153
	 * This method is required by IService interface and is invoked by application.
154
	 * @param \Prado\Xml\TXmlElement $config service configuration
155
	 */
156
	public function init($config)
157
	{
158
		Prado::trace("Initializing TPageService", '\Prado\Web\Services\TPageService');
159
160
		$pageConfig = $this->loadPageConfig($config);
161
162
		$this->initPageContext($pageConfig);
163
164
		$this->_initialized = true;
165
	}
166
167
	/**
168
	 * Initializes page context.
169
	 * Page context includes path alias settings, namespace usages,
170
	 * parameter initialization, module loadings, page initial properties
171
	 * and authorization rules.
172
	 * @param TPageConfiguration $pageConfig
173
	 */
174
	protected function initPageContext($pageConfig)
175
	{
176
		$application = $this->getApplication();
177
		foreach ($pageConfig->getApplicationConfigurations() as $appConfig) {
178
			$application->applyConfiguration($appConfig);
179
		}
180
181
		$this->applyConfiguration($pageConfig);
182
	}
183
184
	/**
185
	 * Applies a page configuration.
186
	 * @param TPageConfiguration $config the configuration
187
	 */
188
	protected function applyConfiguration($config)
189
	{
190
		// initial page properties (to be set when page runs)
191
		$this->_properties = array_merge($this->_properties, $config->getProperties());
192
		$this->getApplication()->getAuthorizationRules()->mergeWith($config->getRules());
193
		$pagePath = $this->getRequestedPagePath();
194
		// external configurations
195
		foreach ($config->getExternalConfigurations() as $filePath => $params) {
196
			[$configPagePath, $condition] = $params;
197
			if ($condition !== true) {
198
				$condition = $this->evaluateExpression($condition);
199
			}
200
			if ($condition) {
201
				if (($path = Prado::getPathOfNamespace($filePath, Prado::getApplication()->getConfigurationFileExt())) === null || !is_file($path)) {
202
					throw new TConfigurationException('pageservice_includefile_invalid', $filePath);
203
				}
204
				$c = new TPageConfiguration($pagePath);
205
				$c->loadFromFile($path, $configPagePath);
206
				$this->applyConfiguration($c);
207
			}
208
		}
209
	}
210
211
	/**
212
	 * Determines the requested page path.
213
	 * @return string page path requested
214
	 */
215
	protected function determineRequestedPagePath()
216
	{
217
		$pagePath = $this->getRequest()->getServiceParameter();
218
		if (empty($pagePath)) {
219
			$pagePath = $this->getDefaultPage();
220
		}
221
		return $pagePath;
222
	}
223
224
	/**
225
	 * Collects configuration for a page.
226
	 * @param \Prado\Xml\TXmlElement $config additional configuration specified in the application configuration
227
	 * @return TPageConfiguration
228
	 */
229
	protected function loadPageConfig($config)
230
	{
231
		$application = $this->getApplication();
232
		$pagePath = $this->getRequestedPagePath();
233
		$pageConfig = null;
234
		if (($cache = $application->getCache()) === null) {
235
			$pageConfig = new TPageConfiguration($pagePath);
236
			if ($config !== null) {
237
				if ($application->getConfigurationType() == TApplication::CONFIG_TYPE_PHP) {
238
					$pageConfig->loadPageConfigurationFromPhp($config, $application->getBasePath(), '');
239
				} else {
240
					$pageConfig->loadPageConfigurationFromXml($config, $application->getBasePath(), '');
241
				}
242
			}
243
			$pageConfig->loadFromFiles($this->getBasePath());
244
		} else {
245
			$configCached = true;
246
			$currentTimestamp = [];
247
			$arr = $cache->get(self::CONFIG_CACHE_PREFIX . $this->getID() . $pagePath);
248
			if (is_array($arr)) {
249
				[$pageConfig, $timestamps] = $arr;
250
				if ($application->getMode() !== TApplicationMode::Performance) {
251
					foreach ($timestamps as $fileName => $timestamp) {
252
						if ($fileName === 0) { // application config file
253
							$appConfigFile = $application->getConfigurationFile();
254
							$currentTimestamp[0] = $appConfigFile === null ? 0 : @filemtime($appConfigFile);
255
							if ($currentTimestamp[0] > $timestamp || ($timestamp > 0 && !$currentTimestamp[0])) {
256
								$configCached = false;
257
							}
258
						} else {
259
							$currentTimestamp[$fileName] = @filemtime($fileName);
260
							if ($currentTimestamp[$fileName] > $timestamp || ($timestamp > 0 && !$currentTimestamp[$fileName])) {
261
								$configCached = false;
262
							}
263
						}
264
					}
265
				}
266
			} else {
267
				$configCached = false;
268
				$paths = explode('.', $pagePath);
269
				$configPath = $this->getBasePath();
270
				$fileName = $this->getApplication()->getConfigurationType() == TApplication::CONFIG_TYPE_PHP
271
					? self::CONFIG_FILE_PHP
272
					: self::CONFIG_FILE_XML;
273
				foreach ($paths as $path) {
274
					$configFile = $configPath . DIRECTORY_SEPARATOR . $fileName;
275
					$currentTimestamp[$configFile] = @filemtime($configFile);
276
					$configPath .= DIRECTORY_SEPARATOR . $path;
277
				}
278
				$appConfigFile = $application->getConfigurationFile();
279
				$currentTimestamp[0] = $appConfigFile === null ? 0 : @filemtime($appConfigFile);
0 ignored issues
show
introduced by
The condition $appConfigFile === null is always false.
Loading history...
280
			}
281
			if (!$configCached) {
282
				$pageConfig = new TPageConfiguration($pagePath);
283
				if ($config !== null) {
284
					if ($application->getConfigurationType() == TApplication::CONFIG_TYPE_PHP) {
285
						$pageConfig->loadPageConfigurationFromPhp($config, $application->getBasePath(), '');
286
					} else {
287
						$pageConfig->loadPageConfigurationFromXml($config, $application->getBasePath(), '');
288
					}
289
				}
290
				$pageConfig->loadFromFiles($this->getBasePath());
291
				$cache->set(self::CONFIG_CACHE_PREFIX . $this->getID() . $pagePath, [$pageConfig, $currentTimestamp]);
292
			}
293
		}
294
		return $pageConfig;
295
	}
296
297
	/**
298
	 * @return TTemplateManager template manager
299
	 */
300
	public function getTemplateManager()
301
	{
302
		if (!$this->_templateManager) {
303
			$this->_templateManager = new TTemplateManager;
304
			$this->_templateManager->init(null);
305
		}
306
		return $this->_templateManager;
307
	}
308
309
	/**
310
	 * @param TTemplateManager $value template manager
311
	 */
312
	public function setTemplateManager(TTemplateManager $value)
313
	{
314
		$this->_templateManager = $value;
315
	}
316
317
	/**
318
	 * @return TThemeManager theme manager
319
	 */
320
	public function getThemeManager()
321
	{
322
		if (!$this->_themeManager) {
323
			$this->_themeManager = new TThemeManager;
324
			$this->_themeManager->init(null);
325
		}
326
		return $this->_themeManager;
327
	}
328
329
	/**
330
	 * @param TThemeManager $value theme manager
331
	 */
332
	public function setThemeManager(TThemeManager $value)
333
	{
334
		$this->_themeManager = $value;
335
	}
336
337
	/**
338
	 * @return string the requested page path
339
	 */
340
	public function getRequestedPagePath()
341
	{
342
		if ($this->_pagePath === null) {
343
			$this->_pagePath = strtr($this->determineRequestedPagePath(), '/\\', '..');
344
			if (empty($this->_pagePath)) {
345
				throw new THttpException(404, 'pageservice_page_required');
346
			}
347
		}
348
		return $this->_pagePath;
349
	}
350
351
	/**
352
	 * @return TPage the requested page
353
	 */
354
	public function getRequestedPage()
355
	{
356
		return $this->_page;
357
	}
358
359
	/**
360
	 * @return string default page path to be served if no explicit page is request. Defaults to 'Home'.
361
	 */
362
	public function getDefaultPage()
363
	{
364
		return $this->_defaultPage;
365
	}
366
367
	/**
368
	 * @param string $value default page path to be served if no explicit page is request
369
	 * @throws TInvalidOperationException if the page service is initialized
370
	 */
371
	public function setDefaultPage($value)
372
	{
373
		if ($this->_initialized) {
374
			throw new TInvalidOperationException('pageservice_defaultpage_unchangeable');
375
		} else {
376
			$this->_defaultPage = $value;
377
		}
378
	}
379
380
	/**
381
	 * @return string the URL for the default page
382
	 */
383
	public function getDefaultPageUrl()
384
	{
385
		return $this->constructUrl($this->getDefaultPage());
386
	}
387
388
	/**
389
	 * @return string the root directory for storing pages. Defaults to the 'pages' directory under the application base path.
390
	 */
391
	public function getBasePath()
392
	{
393
		if ($this->_basePath === null) {
394
			$basePath = $this->getApplication()->getBasePath() . DIRECTORY_SEPARATOR . self::DEFAULT_BASEPATH;
395
			if (($this->_basePath = realpath($basePath)) === false || !is_dir($this->_basePath)) {
396
				$basePath = $this->getApplication()->getBasePath() . DIRECTORY_SEPARATOR . self::FALLBACK_BASEPATH;
397
				if (($this->_basePath = realpath($basePath)) === false || !is_dir($this->_basePath)) {
398
					throw new TConfigurationException('pageservice_basepath_invalid', $basePath);
399
				}
400
			}
401
		}
402
		return $this->_basePath;
403
	}
404
405
	/**
406
	 * @param string $value root directory (in namespace form) storing pages
407
	 * @throws TInvalidOperationException if the service is initialized already or basepath is invalid
408
	 */
409
	public function setBasePath($value)
410
	{
411
		if ($this->_initialized) {
412
			throw new TInvalidOperationException('pageservice_basepath_unchangeable');
413
		} elseif (($path = Prado::getPathOfNamespace($value)) === null || !is_dir($path)) {
414
			throw new TConfigurationException('pageservice_basepath_invalid', $value);
415
		}
416
		$this->_basePath = realpath($path);
417
	}
418
419
	/**
420
	 * Sets the base page class name (in namespace format).
421
	 * If a page only has a template file without page class file,
422
	 * this base page class will be instantiated.
423
	 * @param string $value class name
424
	 */
425
	public function setBasePageClass($value)
426
	{
427
		$this->_basePageClass = $value;
428
	}
429
430
	/**
431
	 * @return string base page class name in namespace format. Defaults to 'TPage'.
432
	 */
433
	public function getBasePageClass()
434
	{
435
		return $this->_basePageClass;
436
	}
437
438
	/**
439
	 * Sets the clientscript manager class (in namespace format).
440
	 * @param string $value class name
441
	 * @since 3.1.7
442
	 */
443
	public function setClientScriptManagerClass($value)
444
	{
445
		$this->_clientScriptManagerClass = $value;
446
	}
447
448
	/**
449
	 * @return string clientscript manager class in namespace format. Defaults to 'Prado\Web\UI\TClientScriptManager'.
450
	 * @since 3.1.7
451
	 */
452
	public function getClientScriptManagerClass()
453
	{
454
		return $this->_clientScriptManagerClass;
455
	}
456
457
	/**
458
	 * Runs the service.
459
	 * This will create the requested page, initializes it with the property values
460
	 * specified in the configuration, and executes the page.
461
	 */
462
	public function run()
463
	{
464
		Prado::trace("Running page service", 'Prado\Web\Services\TPageService');
465
		$this->_page = $this->createPage($this->getRequestedPagePath());
466
		$this->runPage($this->_page, $this->_properties);
467
	}
468
469
	/**
470
	 * Creates a page instance based on requested page path.  If the Page is not
471
	 * found in the BasePath then this method raises onAdditionalPagePaths($pagePath)
472
	 * to query for any additional page paths. eg. from composer package modules.
473
	 * @param string $pagePath requested page path
474
	 * @throws THttpException if requested page path is invalid
475
	 * @throws TConfigurationException if the page class cannot be found
476
	 * @return TPage the requested page instance
477
	 */
478
	protected function createPage($pagePath)
479
	{
480
		$path = $this->getBasePath() . DIRECTORY_SEPARATOR . strtr($pagePath, '.', DIRECTORY_SEPARATOR);
481
		$hasTemplateFile = is_file($path . self::PAGE_FILE_EXT);
482
		$hasClassFile = is_file($path . Prado::CLASS_FILE_EXT);
483
484
		if (!$hasTemplateFile && !$hasClassFile) {
485
			$paths = $this->onAdditionalPagePaths($pagePath);
486
			$applicationPath = Prado::getPathOfAlias('Application') ?? '';
487
			$throwException = true;
488
			foreach ($paths as $path) {
489
				if (stripos($path, $applicationPath) !== 0) {
490
					throw new THttpException(403, 'pageservice_security_violation', $path);
491
				}
492
				$hasTemplateFile = is_file($path . self::PAGE_FILE_EXT);
493
				$hasClassFile = is_file($path . Prado::CLASS_FILE_EXT);
494
				if ($hasTemplateFile || $hasClassFile) {
495
					$throwException = false;
496
					break;
497
				}
498
			}
499
			if ($throwException) {
500
				throw new THttpException(404, 'pageservice_page_unknown', $pagePath);
501
			}
502
		}
503
504
		if ($hasClassFile) {
505
			$className = basename($path);
506
			$namespacedClassName = static::PAGE_NAMESPACE_PREFIX . str_replace('.', '\\', $pagePath);
507
508
			if (!class_exists($className, false) && !class_exists($namespacedClassName, false)) {
509
				include_once($path . Prado::CLASS_FILE_EXT);
510
			}
511
512
			if (!class_exists($className, false)) {
513
				$className = $namespacedClassName;
514
			}
515
		} else {
516
			$className = $this->getBasePageClass();
517
			Prado::using($className);
518
			if (($pos = strrpos($className, '.')) !== false) {
519
				$className = substr($className, $pos + 1);
520
			}
521
		}
522
523
		if ($className !== '\Prado\Web\UI\TPage' && !is_subclass_of($className, '\Prado\Web\UI\TPage')) {
524
			throw new THttpException(404, 'pageservice_page_unknown', $pagePath);
525
		}
526
527
		$page = Prado::createComponent($className);
528
		$page->setPagePath($pagePath);
529
530
		if ($hasTemplateFile) {
531
			$page->setTemplate($this->getTemplateManager()->getTemplateByFileName($path . self::PAGE_FILE_EXT));
532
		}
533
534
		return $page;
535
	}
536
537
	/**
538
	 * Executes a page.  Between setting the subproperties and running the page, onPreRunPage event is raised
539
	 * @param TPage $page the page instance to be run
540
	 * @param array $properties list of initial page properties
541
	 */
542
	protected function runPage($page, $properties)
543
	{
544
		foreach ($properties as $name => $value) {
545
			$page->setSubProperty($name, $value);
546
		}
547
		$this->onPreRunPage($page);
548
		$page->run($this->getResponse()->createHtmlWriter());
549
	}
550
	
551
	/**
552
	 * This event is raised if the page is not found in the BasePath.
553
	 * This provides additional possible Page Paths to look for the page.
554
	 * The typical handler would look like:
555
	 * <code>
556
	 * public function additionalPagePaths($service, $pagePath)
557
	 * {
558
	 *	 return $this->getPluginPagesPath() . DIRECTORY_SEPARATOR . strtr($pagePath, '.', DIRECTORY_SEPARATOR);
559
	 * }
560
	 * </code>
561
	 * @param mixed $param what is passed as the parameter to the event
562
	 * @since 4.2.0
563
	 */
564
	public function onAdditionalPagePaths($param)
565
	{
566
		return $this->raiseEvent('onAdditionalPagePaths', $this, $param);
567
	}
568
	
569
	/**
570
	 * This event is raised just before the page is run.  Any part of the system can  patch into the page
571
	 * events with an onPreRunPage handler.
572
	 * @param mixed $param what is passed as the parameter to the event
573
	 * @since 4.2.0
574
	 */
575
	public function onPreRunPage($param)
576
	{
577
		$this->raiseEvent('onPreRunPage', $this, $param);
578
	}
579
580
	/**
581
	 * Constructs a URL with specified page path and GET parameters.
582
	 * @param string $pagePath page path
583
	 * @param array $getParams list of GET parameters, null if no GET parameters required
584
	 * @param bool $encodeAmpersand whether to encode the ampersand in URL, defaults to true.
585
	 * @param bool $encodeGetItems whether to encode the GET parameters (their names and values), defaults to true.
586
	 * @return string URL for the page and GET parameters
587
	 */
588
	public function constructUrl($pagePath, $getParams = null, $encodeAmpersand = true, $encodeGetItems = true)
589
	{
590
		return $this->getRequest()->constructUrl($this->getID(), $pagePath, $getParams, $encodeAmpersand, $encodeGetItems);
591
	}
592
}
593