Templates::_templateDebug()   B
last analyzed

Complexity

Conditions 8
Paths 2

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 8

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 9
nc 2
nop 2
dl 0
loc 13
rs 8.4444
c 1
b 0
f 0
ccs 1
cts 1
cp 1
crap 8
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 Beta 1
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
	 * Check if a template has already been loaded
106
	 *
107
	 * @param string $template_name Name of the template to check
108 287
	 * @return bool True if template is loaded, false otherwise
109
	 */
110
	public function isTemplateLoaded(string $template_name): bool
111
	{
112
		$template_functions = [
113
			'template_' . $template_name,
114
			'template_' . $template_name . '_above',
115
			'template_' . $template_name . '_below',
116
			'template_' . $template_name . '_init'
117
		];
118
119 287
		foreach ($template_functions as $function)
120
		{
121
			if (function_exists($function))
122
			{
123
				return true;
124
			}
125
		}
126
127
		return false;
128
	}
129
130
	/**
131
	 * <b>Internal function! Do not use it, use theme()->getTemplates()->load instead</b>
132
	 *
133
	 * What it does:
134
	 * - Loads a template file with the name template_name from the current, default, or
135
	 * base theme.
136
	 * - Detects a wrong default theme directory and tries to work around it.
137
	 * - Can be used to only load style sheets by using false as the template name
138
	 *  loading of style sheets with this function is deprecated, use loadCSSFile instead
139
	 *
140
	 * @param string|false $template_name
141
	 * @param string[]|string $style_sheets any style sheets to load with the template
142 287
	 * @param bool $fatal = true if fatal is true, dies with an error message if the
143
	 *     template cannot be found
144 287
	 *
145
	 * @uses $this->dirs->fileInclude() to include the file.
146 287
	 *
147
	 */
148
	protected function requireTemplate($template_name, $style_sheets, $fatal): bool
149
	{
150
		global $context, $settings, $txt, $db_show_debug;
151 287
152
		if ($this->isTemplateLoaded($template_name))
0 ignored issues
show
Bug introduced by
It seems like $template_name can also be of type false; however, parameter $template_name of ElkArte\Themes\Templates::isTemplateLoaded() 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

152
		if ($this->isTemplateLoaded(/** @scrutinizer ignore-type */ $template_name))
Loading history...
153 229
		{
154 229
			return true;
155
		}
156
157
		if (!is_array($style_sheets))
158 287
		{
159
			$style_sheets = [$style_sheets];
160
		}
161
162
		if (!$this->default_loaded)
163
		{
164
			loadCSSFile('index.css');
165
			$this->default_loaded = true;
166
		}
167
168
		// Any specific template style sheets to load?
169
		if (!empty($style_sheets))
170
		{
171
			trigger_error(
172
				'Use of theme()->getTemplates()->load to add style sheets to the head is deprecated.',
173
				E_USER_DEPRECATED
174
			);
175
			$sheets = [];
176
			foreach ($style_sheets as $sheet)
177
			{
178
				$sheets[] = stripos('.css', $sheet) !== false ? $sheet : $sheet . '.css';
179 287
				if ($sheet !== 'admin')
180
				{
181
					continue;
182
				}
183
184 287
				if (empty($context['theme_variant']))
185 287
				{
186 287
					continue;
187
				}
188 287
189
				$sheets[] = $context['theme_variant'] . '/admin' . $context['theme_variant'] . '.css';
190 287
			}
191 287
192 287
			loadCSSFile($sheets);
193 287
		}
194
195 287
		// No template to load?
196
		if ($template_name === false)
197
		{
198
			return true;
199 287
		}
200
201 287
		$loaded = false;
202
		$template_dir = '';
203
		$file_functions = FileFunctions::instance();
204
		foreach ($this->dirs->getDirectories() as $template_dir)
205
		{
206
			if ($file_functions->fileExists($template_dir . '/' . $template_name . '.template.php'))
207
			{
208
				$loaded = true;
209
				try
210 287
				{
211
					$this->dirs->fileInclude(
212 287
						$template_dir . '/' . $template_name . '.template.php',
213
						true
214
					);
215
				}
216
				catch (Error $e)
217
				{
218
					$this->templateNotFound($e);
219
				}
220
221
				break;
222
			}
223
		}
224
225
		if ($loaded)
226
		{
227
			if ($db_show_debug === true)
228
			{
229
				Debug::instance()->add(
230
					'templates',
231
					$template_name . ' (' . basename($template_dir) . ')'
232
				);
233
			}
234
235
			// If they have specified an initialization function for this template, go ahead and call it now.
236
			if (function_exists('template_' . $template_name . '_init'))
237
			{
238
				call_user_func('template_' . $template_name . '_init');
239
			}
240
		}
241
		// Hmmm... doesn't exist?!  I don't suppose the directory is wrong, is it?
242
		elseif (!$file_functions->fileExists($settings['default_theme_dir'])
243
			&& $file_functions->fileExists(BOARDDIR . '/themes/default'))
244
		{
245
			$settings['default_theme_dir'] = BOARDDIR . '/themes/default';
246
			$this->dirs->addDirectory($settings['default_theme_dir']);
247
248
			if (!empty($context['user']['is_admin']) && !isset($_GET['th']))
249
			{
250
				Txt::load('Errors');
251
252
				if (!isset($context['security_controls_files']['title']))
253
				{
254
					$context['security_controls_files']['title'] = $txt['generic_warning'];
255
				}
256
257
				$context['security_controls_files']['errors']['theme_dir'] =
258
					'<a href="' . getUrl('admin', ['action' => 'admin', 'area' => 'theme', 'sa' => 'list', 'th' => 1, '{session_data}']) . '">' . $txt['theme_dir_wrong'] . '</a>';
259
			}
260
261
			$this->load($template_name);
262 287
		}
263
		// Cause an error otherwise.
264
		elseif ($template_name !== 'Errors' && $template_name !== 'index' && $fatal)
265
		{
266
			throw new Exception(
267
				'theme_template_error',
268
				'template',
269
				[(string) $template_name]
270
			);
271
		}
272
		elseif ($fatal)
273
		{
274
			throw new Exception(
275
				sprintf(
276
					$txt['theme_template_error'] ?? 'Unable to load themes/default/%s.template.php!',
277
					(string) $template_name
278 341
				), 'template'
279
			);
280
		}
281
		else
282
		{
283
			return false;
284
		}
285 341
286
		return true;
287
	}
288 341
289
	/**
290 64
	 * Displays an error when a template is not found or has syntax errors preventing its
291
	 * loading
292
	 *
293
	 * @param Error $e
294
	 */
295 295
	protected function templateNotFound(Error $e): never
296
	{
297
		global $context, $txt, $scripturl, $boardurl;
298
299 295
		obStart();
300
301
		// Don't cache error pages!!
302
		Headers::instance()
303 295
			->removeHeader('all')
304
			->header('Expires', 'Mon, 26 Jul 1997 05:00:00 GMT')
305 239
			->header('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT')
306
			->header('Cache-Control', 'no-cache')
307 261
			->contentType('text/html', 'UTF-8')
308
			->sendHeaders();
309 295
310
		if (!isset($txt['template_parse_error']))
311
		{
312
			$txt['template_parse_error'] = 'Template Parse Error!';
313
			$txt['template_parse_error_message'] =
314
				'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>.';
315
			$txt['template_parse_error_details'] =
316 295
				'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>.';
317
			$txt['template_parse_undefined'] =
318
				'An undefined error occurred during the parsing of this template';
319
		}
320
321
		// First, let's get the doctype and language information out of the way.
322
		echo '<!DOCTYPE html>
323
<html ', empty($context['right_to_left']) ? '' : 'dir="rtl"', '>
324
	<head>
325
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
326
		<style>
327
			body {
328
				color: #222;
329
				background-color: #FAFAFA;
330
				font-family: Verdana, arial, helvetica, serif;
331
				font-size: small;
332
			}
333
			a {color: #49643D;}
334
			.curline {background: #ffe; display: inline-block;}
335
			.lineno {color:#222; -webkit-user-select: none;-moz-user-select: none; -ms-user-select: none;user-select: none;}
336
		</style>';
337
338
		if (!allowedTo('admin_forum'))
339
		{
340
			echo '
341
		<title>', $txt['template_parse_error'], '</title>
342
	</head>
343
	<body>
344
		<h3>', $txt['template_parse_error'], '</h3>
345
		', $txt['template_parse_error_message'], '
346
	</body>
347
</html>';
348
		}
349
		else
350
		{
351
			$error = $e->getMessage();
352
353
			echo '
354
		<title>', $txt['template_parse_error'], '</title>
355
	</head>
356
	<body>
357
		<h3>', $txt['template_parse_error'], '</h3>
358
		', sprintf(
359
				$txt['template_parse_error_details'],
360
				strtr(
361
					$e->getFile(),
362
					[
363
						BOARDDIR => '',
364
						str_replace('\\', '/', BOARDDIR) => '',
365
					]
366
				),
367
				$boardurl,
368
				$scripturl . '?theme=1'
369
			);
370
371
			echo '
372
		<hr />
373
374
		<div style="margin: 0 20px;"><span style="font-family: monospace;">', str_replace('\\', '/', strtr(
375
				$error,
376
				[
377
					'<strong>' . BOARDDIR => '<strong>...',
378
					'<strong>' . str_replace('\\', '/', BOARDDIR) => '<strong>...',
379
				]
380
			)), '</span></div>';
381
382
			$this->printLines($e);
383
384
			echo '
385
	</body>
386
</html>';
387
		}
388
389
		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...
390
	}
391
392
	/**
393
	 * Print lines from the file with the error.
394
	 *
395
	 * @param Error $e
396
	 * @uses getHighlightedLinesFromFile() Highlights syntax.
397
	 *
398
	 */
399
	private function printLines(Error $e): void
400
	{
401
		if (allowedTo('admin_forum'))
402
		{
403
			$data = iterator_to_array(
404
				$this->getHighlightedLinesFromFile(
405
					$e->getFile(),
406
					max($e->getLine() - 9, 1),
407
					min($e->getLine() + 4, count(file($e->getFile())) + 1)
408
				)
409
			);
410
411
			// Mark the offending line.
412
			$data[$e->getLine()] = sprintf(
413
				'<div class="curline">%s</div>',
414
				$data[$e->getLine()]
415
			);
416
417
			echo '
418
		<div style="margin: 2ex 20px; width: 96%; overflow: auto;"><pre style="margin: 0;">';
419
420
			// Show the relevant lines...
421
			foreach ($data as $line => $content)
422
			{
423
				printf(
424
					'<span class="lineno">%d:</span> ',
425
					$line
426
				);
427
428
				echo $content, "\n";
429
			}
430
431
			echo '</pre></div>';
432
		}
433
	}
434
435
	/**
436
	 * Highlights PHP syntax.
437
	 *
438
	 * @param string $file Name of file to highlight.
439
	 * @param int $min Minimum line number to return.
440
	 * @param int $max Maximum line number to return.
441
	 *
442
	 * @used-by printLines() Prints syntax for template files with errors.
443
	 * @return Generator Highlighted lines ranging from $min to $max.
444
	 * @uses highlight_file() Highlights syntax.
445
	 *
446
	 */
447
	public function getHighlightedLinesFromFile(string $file, int $min, int $max): Generator
448
	{
449
		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

449
		foreach (preg_split('~<br( /)?>~', /** @scrutinizer ignore-type */ highlight_file($file, true)) as $line => $content)
Loading history...
450
		{
451
			if ($line < $min)
452
			{
453
				continue;
454
			}
455
456
			if ($line > $max)
457
			{
458
				continue;
459
			}
460
461
			yield $line + 1 => $content;
462
		}
463
	}
464
465
	/**
466
	 * Load a sub-template.
467
	 *
468
	 * What it does:
469
	 * - loads the sub template specified by sub_template_name, which must be in an
470
	 * already-loaded template.
471
	 * - if ?debug is in the query string, shows administrators a marker after every sub
472
	 * template for debugging purposes.
473
	 *
474
	 * @param string $sub_template_name
475
	 * @param bool|string $fatal = false, $fatal = true is for templates that
476
	 *                           shouldn't get a 'pretty' error screen 'ignore' to skip
477
	 *
478
	 * @throws Exception theme_template_error
479
	 *
480
	 */
481
	public function loadSubTemplate($sub_template_name, $fatal = false): void
482
	{
483
		global $txt, $db_show_debug;
484
485
		if (!empty($sub_template_name))
486
		{
487
			if ($db_show_debug === true)
488
			{
489
				Debug::instance()->add('sub_templates', $sub_template_name);
490
			}
491
492
			// Figure out what the template function is named.
493
			$theme_function = 'template_' . $sub_template_name;
494
495
			if (function_exists($theme_function))
496
			{
497
				try
498
				{
499
					$this->_templateDebug($sub_template_name, true);
500 229
					$theme_function();
501
				}
502 229
				catch (Error $e)
503
				{
504 229
					$this->templateNotFound($e);
505
				}
506
			}
507
			elseif ($fatal === false)
508
			{
509
				throw new Exception(
510
					'theme_template_error',
511
					'template',
512
					[(string) $sub_template_name]
513
				);
514
			}
515
			elseif ($fatal !== 'ignore')
516
			{
517
				throw new BadFunctionCallException(
518
					Errors::instance()->log_error(
519 283
						sprintf(
520
							$txt['theme_template_error'] ?? 'Unable to load the %s sub template!',
521
							(string) $sub_template_name
522
						),
523
						'template'
524
					)
525 283
				);
526 283
			}
527 140
		}
528 140
529 140
		$this->_templateDebug($sub_template_name);
530
	}
531
532
	/**
533
	 * Are we showing debugging for templates?  Just make sure not to do it before the doctype...
534
	 *
535
	 * @param bool $start
536
	 * @param string $sub_template_name
537
	 */
538
	private function _templateDebug($sub_template_name, $start = false): void
539
	{
540
		$req = HttpReq::instance();
541
542
		if ($req->isSet('debug')
543
			&& $sub_template_name !== 'init'
544
			&& ob_get_length() > 0
545 305
			&& empty($req->getRequest('xml'))
546
			&& empty($req->getRequest('api'))
547
			&& allowedTo('admin_forum'))
548
		{
549
			echo '
550
 				<div class="warningbox">---- ', $sub_template_name, ' ', ($start ? 'starts' : 'ends'), ' ----</div>';
551 305
		}
552 305
	}
553 305
554
	/**
555
	 * @return Directories
556 305
	 */
557
	public function getDirectory(): Directories
558 287
	{
559
		return $this->dirs;
560
	}
561
}
562