Passed
Pull Request — development (#3800)
by Spuds
20:37
created

Templates   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 494
Duplicated Lines 0 %

Test Coverage

Coverage 29.27%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 185
dl 0
loc 494
rs 5.5199
c 1
b 0
f 0
ccs 48
cts 164
cp 0.2927
wmc 56

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A getDirectory() 0 3 1
A templateNotFound() 0 95 4
A getHighlightedLinesFromFile() 0 15 4
B loadSubTemplate() 0 49 7
A printLines() 0 33 3
F requireTemplate() 0 134 24
A load() 0 27 4
B _templateDebug() 0 13 8

How to fix   Complexity   

Complex Class

Complex classes like Templates 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.

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 Templates, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * This file has functions dealing with loading and precessing template files.
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 dev
14
 *
15
 */
16
17
namespace ElkArte\Themes;
18
19
use BadFunctionCallException;
20
use ElkArte\Debug;
21
use ElkArte\Errors\Errors;
0 ignored issues
show
Bug introduced by
The type ElkArte\Errors\Errors was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
22
use ElkArte\Exceptions\Exception;
23
use ElkArte\Helper\FileFunctions;
24
use ElkArte\Helper\HttpReq;
25
use ElkArte\Http\Headers;
26
use ElkArte\Languages\Txt;
27
use Error;
28
use Generator;
29
30
/**
31
 * Class Templates
32
 *
33
 * This class loads and processes template files and sheets.
34
 */
35
class Templates
36
{
37
	/** @var Directories Template directory's that we will be searching for the sheets */
38
	public $dirs;
39
40
	/** @var array Template sheets that have not loaded */
41
	protected $delayed = [];
42
43
	/** @var bool Tracks if the default index.css has been loaded */
44
	protected $default_loaded = false;
45
46
	/**
47
	 * Templates constructor.
48
	 */
49
	public function __construct(Directories $dirs)
50
	{
51
		$this->dirs = $dirs;
52
53
		// We want to be able to figure out any errors...
54
		error_clear_last();
55
	}
56
57
	/**
58
	 * Load a template - if the theme doesn't include it, use the default.
59
	 *
60
	 * What it does:
61
	 * - Loads a template file with the name template_name from the current, default, or base theme.
62
	 * - Detects a wrong default theme directory and tries to work around it.
63
	 * - Can be used to only load style sheets by using false as the template name
64
	 *   loading of style sheets with this function is deprecated, use loadCSSFile instead
65
	 * - If $this->dirs is empty, it delays the loading of the template
66 229
	 *
67
	 * @param string|false $template_name
68
	 * @param string[]|string $style_sheets any style sheets to load with the template
69 229
	 * @param bool $fatal = true if fatal is true, dies with an error message if the
70 229
	 *     template cannot be found
71
	 *
72
	 * @uses $this->requireTemplate() to actually load the file.
73
	 *
74
	 */
75
	public function load($template_name, $style_sheets = [], $fatal = true): ?bool
76
	{
77
		// If we don't know yet the default theme directory, let's wait a bit.
78
		if ($this->dirs->hasDirectories() === false)
79
		{
80
			$this->delayed[] = [
81
				$template_name,
82
				$style_sheets,
83
				$fatal,
84
			];
85
86
			return null;
87
		}
88
89
		// If instead we know the default theme directory, and we have delayed something, it's time to process
90
		if (!empty($this->delayed))
91
		{
92
			foreach ($this->delayed as $val)
93
			{
94 287
				$this->requireTemplate($val[0], $val[1], $val[2]);
95
			}
96
97 287
			// Forget about them (load them only once)
98
			$this->delayed = [];
99
		}
100
101
		return $this->requireTemplate($template_name, $style_sheets, $fatal);
102
	}
103
104
	/**
105
	 * <b>Internal function! Do not use it, use theme()->getTemplates()->load instead</b>
106
	 *
107
	 * What it does:
108 287
	 * - Loads a template file with the name template_name from the current, default, or
109
	 * base theme.
110
	 * - Detects a wrong default theme directory and tries to work around it.
111
	 * - Can be used to only load style sheets by using false as the template name
112
	 *  loading of style sheets with this function is deprecated, use loadCSSFile instead
113
	 *
114
	 * @param string|false $template_name
115
	 * @param string[]|string $style_sheets any style sheets to load with the template
116
	 * @param bool $fatal = true if fatal is true, dies with an error message if the
117
	 *     template cannot be found
118
	 *
119 287
	 * @uses $this->dirs->fileInclude() to include the file.
120
	 *
121
	 */
122
	protected function requireTemplate($template_name, $style_sheets, $fatal): bool
123
	{
124
		global $context, $settings, $txt, $db_show_debug;
125
126
		if (!is_array($style_sheets))
127
		{
128
			$style_sheets = [$style_sheets];
129
		}
130
131
		if (!$this->default_loaded)
132
		{
133
			loadCSSFile('index.css');
134
			$this->default_loaded = true;
135
		}
136
137
		// Any specific template style sheets to load?
138
		if (!empty($style_sheets))
139
		{
140
			trigger_error(
141
				'Use of theme()->getTemplates()->load to add style sheets to the head is deprecated.',
142 287
				E_USER_DEPRECATED
143
			);
144 287
			$sheets = [];
145
			foreach ($style_sheets as $sheet)
146 287
			{
147
				$sheets[] = stripos('.css', $sheet) !== false ? $sheet : $sheet . '.css';
148
				if ($sheet !== 'admin')
149
				{
150
					continue;
151 287
				}
152
153 229
				if (empty($context['theme_variant']))
154 229
				{
155
					continue;
156
				}
157
158 287
				$sheets[] = $context['theme_variant'] . '/admin' . $context['theme_variant'] . '.css';
159
			}
160
161
			loadCSSFile($sheets);
162
		}
163
164
		// No template to load?
165
		if ($template_name === false)
166
		{
167
			return true;
168
		}
169
170
		$loaded = false;
171
		$template_dir = '';
172
		$file_functions = FileFunctions::instance();
173
		foreach ($this->dirs->getDirectories() as $template_dir)
174
		{
175
			if ($file_functions->fileExists($template_dir . '/' . $template_name . '.template.php'))
176
			{
177
				$loaded = true;
178
				try
179 287
				{
180
					$this->dirs->fileInclude(
181
						$template_dir . '/' . $template_name . '.template.php',
182
						true
183
					);
184 287
				}
185 287
				catch (Error $e)
186 287
				{
187
					$this->templateNotFound($e);
188 287
				}
189
190 287
				break;
191 287
			}
192 287
		}
193 287
194
		if ($loaded)
195 287
		{
196
			if ($db_show_debug === true)
197
			{
198
				Debug::instance()->add(
199 287
					'templates',
200
					$template_name . ' (' . basename($template_dir) . ')'
201 287
				);
202
			}
203
204
			// If they have specified an initialization function for this template, go ahead and call it now.
205
			if (function_exists('template_' . $template_name . '_init'))
206
			{
207
				call_user_func('template_' . $template_name . '_init');
208
			}
209
		}
210 287
		// Hmmm... doesn't exist?!  I don't suppose the directory is wrong, is it?
211
		elseif (!$file_functions->fileExists($settings['default_theme_dir'])
212 287
			&& $file_functions->fileExists(BOARDDIR . '/themes/default'))
213
		{
214
			$settings['default_theme_dir'] = BOARDDIR . '/themes/default';
215
			$this->dirs->addDirectory($settings['default_theme_dir']);
216
217
			if (!empty($context['user']['is_admin']) && !isset($_GET['th']))
218
			{
219
				Txt::load('Errors');
220
221
				if (!isset($context['security_controls_files']['title']))
222
				{
223
					$context['security_controls_files']['title'] = $txt['generic_warning'];
224
				}
225
226
				$context['security_controls_files']['errors']['theme_dir'] =
227
					'<a href="' . getUrl('admin', ['action' => 'admin', 'area' => 'theme', 'sa' => 'list', 'th' => 1, '{session_data}']) . '">' . $txt['theme_dir_wrong'] . '</a>';
228
			}
229
230
			$this->load($template_name);
231
		}
232
		// Cause an error otherwise.
233
		elseif ($template_name !== 'Errors' && $template_name !== 'index' && $fatal)
234
		{
235
			throw new Exception(
236
				'theme_template_error',
237
				'template',
238
				[(string) $template_name]
239
			);
240
		}
241
		elseif ($fatal)
242
		{
243
			throw new Exception(
244
				sprintf(
245
					$txt['theme_template_error'] ?? 'Unable to load themes/default/%s.template.php!',
246
					(string) $template_name
247
				), 'template'
248
			);
249
		}
250
		else
251
		{
252
			return false;
253
		}
254
255
		return true;
256
	}
257
258
	/**
259
	 * Displays an error when a template is not found or has syntax errors preventing its
260
	 * loading
261
	 *
262 287
	 * @param Error $e
263
	 */
264
	protected function templateNotFound(Error $e)
265
	{
266
		global $context, $txt, $scripturl, $boardurl;
267
268
		obStart();
269
270
		// Don't cache error pages!!
271
		Headers::instance()
272
			->removeHeader('all')
273
			->header('Expires', 'Mon, 26 Jul 1997 05:00:00 GMT')
274
			->header('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT')
275
			->header('Cache-Control', 'no-cache')
276
			->contentType('text/html', 'UTF-8')
277
			->sendHeaders();
278 341
279
		if (!isset($txt['template_parse_error']))
280
		{
281
			$txt['template_parse_error'] = 'Template Parse Error!';
282
			$txt['template_parse_error_message'] =
283
				'It seems something has gone sour on the forum with the template system.  This problem should only be temporary, so please come back later and try again.  If you continue to see this message, please contact the administrator.<br /><br />You can also try <a href="javascript:location.reload();">refreshing this page</a>.';
284
			$txt['template_parse_error_details'] =
285 341
				'There was a problem loading the <span style="font-family: monospace;"><strong>%1$s</strong></span> template or language file.  Please check the syntax and try again - remember, single quotes (<span style="font-family: monospace;">\'</span>) often have to be escaped with a slash (<span style="font-family: monospace;">\\</span>).  To see more specific error information from PHP, try <a href="%2$s%1$s" class="extern">accessing the file directly</a>.<br /><br />You may want to try to <a href="javascript:location.reload();">refresh this page</a> or <a href="%3$s">use the default theme</a>.';
286
			$txt['template_parse_undefined'] =
287
				'An undefined error occurred during the parsing of this template';
288 341
		}
289
290 64
		// First, let's get the doctype and language information out of the way.
291
		echo '<!DOCTYPE html>
292
<html ', empty($context['right_to_left']) ? '' : 'dir="rtl"', '>
293
	<head>
294
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
295 295
		<style>
296
			body {
297
				color: #222;
298
				background-color: #FAFAFA;
299 295
				font-family: Verdana, arial, helvetica, serif;
300
				font-size: small;
301
			}
302
			a {color: #49643D;}
303 295
			.curline {background: #ffe; display: inline-block;}
304
			.lineno {color:#222; -webkit-user-select: none;-moz-user-select: none; -ms-user-select: none;user-select: none;}
305 239
		</style>';
306
307 261
		if (!allowedTo('admin_forum'))
308
		{
309 295
			echo '
310
		<title>', $txt['template_parse_error'], '</title>
311
	</head>
312
	<body>
313
		<h3>', $txt['template_parse_error'], '</h3>
314
		', $txt['template_parse_error_message'], '
315
	</body>
316 295
</html>';
317
		}
318
		else
319
		{
320
			$error = $e->getMessage();
321
322
			echo '
323
		<title>', $txt['template_parse_error'], '</title>
324
	</head>
325
	<body>
326
		<h3>', $txt['template_parse_error'], '</h3>
327
		', sprintf(
328
				$txt['template_parse_error_details'],
329
				strtr(
330
					$e->getFile(),
331
					[
332
						BOARDDIR => '',
333
						strtr(BOARDDIR, '\\', '/') => '',
334
					]
335
				),
336
				$boardurl,
337
				$scripturl . '?theme=1'
338
			);
339
340
			echo '
341
		<hr />
342
343
		<div style="margin: 0 20px;"><span style="font-family: monospace;">', str_replace('\\', '/', strtr(
344
				$error,
345
				[
346
					'<strong>' . BOARDDIR => '<strong>...',
347
					'<strong>' . strtr(BOARDDIR, '\\', '/') => '<strong>...',
348
				]
349
			)), '</span></div>';
350
351
			$this->printLines($e);
352
353
			echo '
354
	</body>
355
</html>';
356
		}
357
358
		die;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
359
	}
360
361
	/**
362
	 * Print lines from the file with the error.
363
	 *
364
	 * @param Error $e
365
	 * @uses getHighlightedLinesFromFile() Highlights syntax.
366
	 *
367
	 */
368
	private function printLines(Error $e): void
369
	{
370
		if (allowedTo('admin_forum'))
371
		{
372
			$data = iterator_to_array(
373
				$this->getHighlightedLinesFromFile(
374
					$e->getFile(),
375
					max($e->getLine() - 9, 1),
376
					min($e->getLine() + 4, count(file($e->getFile())) + 1)
377
				)
378
			);
379
380
			// Mark the offending line.
381
			$data[$e->getLine()] = sprintf(
382
				'<div class="curline">%s</div>',
383
				$data[$e->getLine()]
384
			);
385
386
			echo '
387
		<div style="margin: 2ex 20px; width: 96%; overflow: auto;"><pre style="margin: 0;">';
388
389
			// Show the relevant lines...
390
			foreach ($data as $line => $content)
391
			{
392
				printf(
393
					'<span class="lineno">%d:</span> ',
394
					$line
395
				);
396
397
				echo $content, "\n";
398
			}
399
400
			echo '</pre></div>';
401
		}
402
	}
403
404
	/**
405
	 * Highlights PHP syntax.
406
	 *
407
	 * @param string $file Name of file to highlight.
408
	 * @param int $min Minimum line number to return.
409
	 * @param int $max Maximum line number to return.
410
	 *
411
	 * @used-by printLines() Prints syntax for template files with errors.
412
	 * @return Generator Highlighted lines ranging from $min to $max.
413
	 * @uses highlight_file() Highlights syntax.
414
	 *
415
	 */
416
	public function getHighlightedLinesFromFile(string $file, int $min, int $max): Generator
417
	{
418
		foreach (preg_split('~<br( /)?>~', highlight_file($file, true)) as $line => $content)
0 ignored issues
show
Bug introduced by
It seems like highlight_file($file, true) can also be of type true; however, parameter $subject of preg_split() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

418
		foreach (preg_split('~<br( /)?>~', /** @scrutinizer ignore-type */ highlight_file($file, true)) as $line => $content)
Loading history...
419
		{
420
			if ($line < $min)
421
			{
422
				continue;
423
			}
424
425
			if ($line > $max)
426
			{
427
				continue;
428
			}
429
430
			yield $line + 1 => $content;
431
		}
432
	}
433
434
	/**
435
	 * Load a sub-template.
436
	 *
437
	 * What it does:
438
	 * - loads the sub template specified by sub_template_name, which must be in an
439
	 * already-loaded template.
440
	 * - if ?debug is in the query string, shows administrators a marker after every sub
441
	 * template for debugging purposes.
442
	 *
443
	 * @param string $sub_template_name
444
	 * @param bool|string $fatal = false, $fatal = true is for templates that
445
	 *                           shouldn't get a 'pretty' error screen 'ignore' to skip
446
	 *
447
	 * @throws Exception theme_template_error
448
	 *
449
	 */
450
	public function loadSubTemplate($sub_template_name, $fatal = false)
451
	{
452
		global $txt, $db_show_debug;
453
454
		if (!empty($sub_template_name))
455
		{
456
			if ($db_show_debug === true)
457
			{
458
				Debug::instance()->add('sub_templates', $sub_template_name);
459
			}
460
461
			// Figure out what the template function is named.
462
			$theme_function = 'template_' . $sub_template_name;
463
464
			if (function_exists($theme_function))
465
			{
466
				try
467
				{
468
					$this->_templateDebug($sub_template_name, true);
469
					$theme_function();
470
				}
471
				catch (Error $e)
472
				{
473
					$this->templateNotFound($e);
474
				}
475
			}
476
			elseif ($fatal === false)
477
			{
478
				throw new Exception(
479
					'theme_template_error',
480
					'template',
481
					[(string) $sub_template_name]
482
				);
483
			}
484
			elseif ($fatal !== 'ignore')
485
			{
486
				throw new BadFunctionCallException(
487
					Errors::instance()->log_error(
488
						sprintf(
489
							$txt['theme_template_error'] ?? 'Unable to load the %s sub template!',
490
							(string) $sub_template_name
491
						),
492
						'template'
493
					)
494
				);
495
			}
496
		}
497
498
		$this->_templateDebug($sub_template_name);
499
	}
500 229
501
	/**
502 229
	 * Are we showing debugging for templates?  Just make sure not to do it before the doctype...
503
	 *
504 229
	 * @param bool $start
505
	 * @param string $sub_template_name
506
	 */
507
	private function _templateDebug($sub_template_name, $start = false)
508
	{
509
		$req = HttpReq::instance();
510
511
		if ($req->isSet('debug')
512
			&& $sub_template_name !== 'init'
513
			&& ob_get_length() > 0
514
			&& empty($req->getRequest('xml'))
515
			&& empty($req->getRequest('api'))
516
			&& allowedTo('admin_forum'))
517
		{
518
			echo '
519 283
 				<div class="warningbox">---- ', $sub_template_name, ' ', ($start ? 'starts' : 'ends'), ' ----</div>';
520
		}
521
	}
522
523
	/**
524
	 * @return Directories
525 283
	 */
526 283
	public function getDirectory()
527 140
	{
528 140
		return $this->dirs;
529 140
	}
530
}
531