Passed
Pull Request — development (#3452)
by Emanuele
06:51
created

Templates::loadLanguageFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 4
dl 0
loc 11
rs 10
c 0
b 0
f 0
ccs 6
cts 6
cp 1
crap 1
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 ElkArte\Themes\Directories;
20
use BadFunctionCallException;
21
use ElkArte\Debug;
22
use ElkArte\Errors\Errors;
23
use ElkArte\Exceptions\Exception;
24
use ElkArte\User;
25
use Error;
26
use Generator;
27
28
29
/**
30
 * Class Templates
31
 *
32
 * This class loads and processes template files and sheets.
33
 */
34
class Templates
35
{
36
	/**
37
	 * Template directory's that we will be searching for the sheets
38
	 *
39
	 * @var \ElkArte\Themes\Directories
40
	 */
41
	public $dirs = null;
42
43
	/**
44
	 * Template sheets that have not loaded
45
	 *
46
	 * @var array
47
	 */
48
	protected $delayed = [];
49
50
	/**
51
	 * Tracks if the default index.css has been loaded
52
	 *
53
	 * @var bool
54
	 */
55
	protected $default_loaded = false;
56
57
	/**
58
	 * Templates constructor.
59
	 */
60
	public function __construct(\ElkArte\Themes\Directories $dirs)
61
	{
62
		$this->dirs = $dirs;
63
64
		// We want to be able to figure out any errors...
65
		error_clear_last();
66 229
	}
67
68
	/**
69 229
	 * Load a template - if the theme doesn't include it, use the default.
70 229
	 *
71
	 * What it does:
72
	 * - Loads a template file with the name template_name from the current, default, or
73
	 * base theme.
74
	 * - Detects a wrong default theme directory and tries to work around it.
75
	 * - Can be used to only load style sheets by using false as the template name
76
	 *   loading of style sheets with this function is deprecated, use loadCSSFile
77
	 * instead
78
	 * - If $this->dirs is empty, it delays the loading of the template
79
	 *
80
	 * @param string|false $template_name
81
	 * @param string[]|string $style_sheets any style sheets to load with the template
82
	 * @param bool $fatal = true if fatal is true, dies with an error message if the
83
	 *     template cannot be found
84
	 *
85
	 * @return bool|null
86
	 * @throws \ElkArte\Exceptions\Exception
87
	 * @uses $this->requireTemplate() to actually load the file.
88
	 *
89
	 */
90
	public function load($template_name, $style_sheets = [], $fatal = true): ?bool
91
	{
92
		// If we don't know yet the default theme directory, let's wait a bit.
93
		if ($this->dirs->hasDirectories() === false)
94 287
		{
95
			$this->delayed[] = [
96
				$template_name,
97 287
				$style_sheets,
98
				$fatal,
99
			];
100
101
			return null;
102
		}
103
		// If instead we know the default theme directory and we have delayed something, it's time to process
104
		elseif (!empty($this->delayed))
105
		{
106
			foreach ($this->delayed as $val)
107
			{
108 287
				$this->requireTemplate($val[0], $val[1], $val[2]);
109
			}
110
111
			// Forget about them (load them only once)
112
			$this->delayed = [];
113
		}
114
115
		return $this->requireTemplate($template_name, $style_sheets, $fatal);
116
	}
117
118
	/**
119 287
	 * <b>Internal function! Do not use it, use theme()->getTemplates()->load instead</b>
120
	 *
121
	 * What it does:
122
	 * - Loads a template file with the name template_name from the current, default, or
123
	 * base theme.
124
	 * - Detects a wrong default theme directory and tries to work around it.
125
	 * - Can be used to only load style sheets by using false as the template name
126
	 *  loading of style sheets with this function is deprecated, use loadCSSFile instead
127
	 *
128
	 * @param string|false $template_name
129
	 * @param string[]|string $style_sheets any style sheets to load with the template
130
	 * @param bool $fatal = true if fatal is true, dies with an error message if the
131
	 *     template cannot be found
132
	 *
133
	 * @return bool
134
	 * @throws \ElkArte\Exceptions\Exception theme_template_error
135
	 * @uses $this->dirs->fileInclude() to include the file.
136
	 *
137
	 */
138
	protected function requireTemplate($template_name, $style_sheets, $fatal): bool
139
	{
140
		global $context, $settings, $txt, $db_show_debug;
141
142 287
		if (!is_array($style_sheets))
143
		{
144 287
			$style_sheets = [$style_sheets];
145
		}
146 287
147
		if (!$this->default_loaded)
148
		{
149
			loadCSSFile('index.css');
150
			$this->default_loaded = true;
151 287
		}
152
153 229
		// Any specific template style sheets to load?
154 229
		if (!empty($style_sheets))
155
		{
156
			trigger_error(
157
				'Use of theme()->getTemplates()->load to add style sheets to the head is deprecated.',
158 287
				E_USER_DEPRECATED
159
			);
160
			$sheets = [];
161
			foreach ($style_sheets as $sheet)
162
			{
163
				$sheets[] = stripos('.css', $sheet) !== false ? $sheet : $sheet . '.css';
164
				if ($sheet === 'admin' && !empty($context['theme_variant']))
165
				{
166
					$sheets[] =
167
						$context['theme_variant'] . '/admin' . $context['theme_variant'] . '.css';
168
				}
169
			}
170
171
			loadCSSFile($sheets);
172
		}
173
174
		// No template to load?
175
		if ($template_name === false)
176
		{
177
			return true;
178
		}
179 287
180
		$loaded = false;
181
		$template_dir = '';
182
		foreach ($this->dirs->getDirectories() as $template_dir)
183
		{
184 287
			if (file_exists($template_dir . '/' . $template_name . '.template.php'))
185 287
			{
186 287
				$loaded = true;
187
				$this->dirs->fileInclude(
188 287
					$template_dir . '/' . $template_name . '.template.php',
189
					true
190 287
				);
191 287
				break;
192 287
			}
193 287
		}
194
195 287
		if ($loaded)
196
		{
197
			if ($db_show_debug === true)
198
			{
199 287
				Debug::instance()->add(
200
					'templates',
201 287
					$template_name . ' (' . basename($template_dir) . ')'
202
				);
203
			}
204
205
			// If they have specified an initialization function for this template, go ahead and call it now.
206
			if (function_exists('template_' . $template_name . '_init'))
207
			{
208
				call_user_func('template_' . $template_name . '_init');
209
			}
210 287
		}
211
		// Hmmm... doesn't exist?!  I don't suppose the directory is wrong, is it?
212 287
		elseif (!file_exists($settings['default_theme_dir']) && file_exists(
213
				BOARDDIR . '/themes/default'
214
			))
215
		{
216
			$settings['default_theme_dir'] = BOARDDIR . '/themes/default';
217
			$this->addDirectory($settings['default_theme_dir']);
0 ignored issues
show
Bug introduced by
The method addDirectory() does not exist on ElkArte\Themes\Templates. ( Ignorable by Annotation )

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

217
			$this->/** @scrutinizer ignore-call */ 
218
          addDirectory($settings['default_theme_dir']);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
218
219
			if (!empty($context['user']['is_admin']) && !isset($_GET['th']))
220
			{
221
				$this->loadLanguageFile('Errors');
0 ignored issues
show
Bug introduced by
The method loadLanguageFile() does not exist on ElkArte\Themes\Templates. ( Ignorable by Annotation )

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

221
				$this->/** @scrutinizer ignore-call */ 
222
           loadLanguageFile('Errors');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
222
223
				if (!isset($context['security_controls_files']['title']))
224
				{
225
					$context['security_controls_files']['title'] =
226
						$txt['generic_warning'];
227
				}
228
229
				$context['security_controls_files']['errors']['theme_dir'] =
230
					'<a href="' . getUrl('admin', ['action' => 'admin', 'area' => 'theme', 'sa' => 'list', 'th' => 1, '{session_data}']) . '">' . $txt['theme_dir_wrong'] . '</a>';
231
			}
232
233
			$this->load($template_name);
234
		}
235
		// Cause an error otherwise.
236
		elseif ($template_name !== 'Errors' && $template_name !== 'index' && $fatal)
237
		{
238
			throw new Exception(
239
				'theme_template_error',
240
				'template',
241
				[(string) $template_name]
242
			);
243
		}
244
		elseif ($fatal)
245
		{
246
			throw new Exception(
247
				sprintf(
248
					isset($txt['theme_template_error']) ? $txt['theme_template_error'] : 'Unable to load themes/default/%s.template.php!',
249
					(string) $template_name
250
				), 'template'
251
			);
252
		}
253
		else
254
		{
255
			return false;
256
		}
257
258
		return true;
259
	}
260
261
	/**
262 287
	 * Displays an error when a template is not found or has syntax errors preventing its
263
	 * loading
264
	 *
265
	 * @param Error $e
266
	 */
267
	protected function templateNotFound(Error $e)
268
	{
269
		global $context, $txt, $scripturl, $boardurl;
270
271
		obStart();
272
273
		// Don't cache error pages!!
274
		header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
275
		header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
276
		header('Cache-Control: no-cache');
277
278 341
		if (!isset($txt['template_parse_error']))
279
		{
280
			$txt['template_parse_error'] = 'Template Parse Error!';
281
			$txt['template_parse_error_message'] =
282
				'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>.';
283
			$txt['template_parse_error_details'] =
284
				'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>.';
285 341
			$txt['template_parse_undefined'] =
286
				'An undefined error occurred during the parsing of this template';
287
		}
288 341
289
		// First, let's get the doctype and language information out of the way.
290 64
		echo '<!DOCTYPE html>
291
<html ', !empty($context['right_to_left']) ? 'dir="rtl"' : '', '>
292
	<head>
293
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
294
		<style>
295 295
			body {
296
				color: #222;
297
				background-color: #FAFAFA;
298
				font-family: Verdana, arial, helvetica, serif;
299 295
				font-size: small;
300
			}
301
			a {color: #49643D;}
302
			.curline {background: #ffe; display: inline-block;}
303 295
			.lineno {color:#222; -webkit-user-select: none;-moz-user-select: none; -ms-user-select: none;user-select: none;}
304
		</style>';
305 239
306
		if (!allowedTo('admin_forum'))
307 261
		{
308
			echo '
309 295
		<title>', $txt['template_parse_error'], '</title>
310
	</head>
311
	<body>
312
		<h3>', $txt['template_parse_error'], '</h3>
313
		', $txt['template_parse_error_message'], '
314
	</body>
315
</html>';
316 295
		}
317
		else
318
		{
319
			$error = $e->getMessage();
320
321
			echo '
322
		<title>', $txt['template_parse_error'], '</title>
323
	</head>
324
	<body>
325
		<h3>', $txt['template_parse_error'], '</h3>
326
		', sprintf(
327
				$txt['template_parse_error_details'],
328
				strtr(
329
					$e->getFile(),
330
					[
331
						BOARDDIR => '',
332
						strtr(BOARDDIR, '\\', '/') => '',
333
					]
334
				),
335
				$boardurl,
336
				$scripturl . '?theme=1'
337
			);
338
339
			echo '
340
		<hr />
341
342
		<div style="margin: 0 20px;"><span style="font-family: monospace;">', strtr(
343
				strtr(
344
					$error,
345
					[
346
						'<strong>' . BOARDDIR => '<strong>...',
347
						'<strong>' . strtr(BOARDDIR, '\\', '/') => '<strong>...',
348
					]
349
				),
350
				'\\',
351
				'/'
352
			), '</span></div>';
353
354
			$this->printLines($e);
355
356
			echo '
357
	</body>
358
</html>';
359
		}
360
361
		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...
362
	}
363
364
	/**
365
	 * Print lines from the file with the error.
366
	 *
367
	 * @param Error $e
368
	 * @uses getHighlightedLinesFromFile() Highlights syntax.
369
	 *
370
	 */
371
	private function printLines(Error $e): void
372
	{
373
		if (allowedTo('admin_forum'))
374
		{
375
			$data = iterator_to_array(
376
				$this->getHighlightedLinesFromFile(
377
					$e->getFile(),
378
					max($e->getLine() - 9, 1),
379
					min($e->getLine() + 4, count(file($e->getFile())) + 1)
380
				)
381
			);
382
383
			// Mark the offending line.
384
			$data[$e->getLine()] = sprintf(
385
				'<div class="curline">%s</div>',
386
				$data[$e->getLine()]
387
			);
388
389
			echo '
390
		<div style="margin: 2ex 20px; width: 96%; overflow: auto;"><pre style="margin: 0;">';
391
392
			// Show the relevant lines...
393
			foreach ($data as $line => $content)
394
			{
395
				printf(
396
					'<span class="lineno">%d:</span> ',
397
					$line
398
				);
399
400
				echo $content, "\n";
401
			}
402
403
			echo '</pre></div>';
404
		}
405
	}
406
407
	/**
408
	 * Highlights PHP syntax.
409
	 *
410
	 * @param string $file Name of file to highlight.
411
	 * @param int $min Minimum line numer to return.
412
	 * @param int $max Maximum line numer to return.
413
	 *
414
	 * @used-by printLines() Prints syntax for template files with errors.
415
	 * @return Generator Highlighted lines ranging from $min to $max.
416
	 * @uses    highlight_file() Highlights syntax.
417
	 *
418
	 */
419
	public function getHighlightedLinesFromFile(
420
		string $file,
421
		int $min,
422
		int $max
423
	): Generator {
424
		foreach (preg_split(
425
					 '~\<br( /)?\>~',
426
					 highlight_file($file, true)
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

426
					 /** @scrutinizer ignore-type */ highlight_file($file, true)
Loading history...
427
				 ) as $line => $content)
428
		{
429
			if ($line >= $min && $line <= $max)
430
			{
431
				yield $line + 1 => $content;
432
			}
433
		}
434
	}
435
436
	/**
437
	 * Load a sub-template.
438
	 *
439
	 * What it does:
440
	 * - loads the sub template specified by sub_template_name, which must be in an
441
	 * already-loaded template.
442
	 * - if ?debug is in the query string, shows administrators a marker after every sub
443
	 * template for debugging purposes.
444
	 *
445
	 * @param string $sub_template_name
446
	 * @param bool|string $fatal = false, $fatal = true is for templates that
447
	 *                           shouldn't get a 'pretty' error screen 'ignore' to skip
448
	 *
449
	 * @throws \ElkArte\Exceptions\Exception theme_template_error
450
	 * @todo get rid of reading $_REQUEST directly
451
	 *
452
	 */
453
	public function loadSubTemplate($sub_template_name, $fatal = false)
454
	{
455
		global $txt, $db_show_debug;
456
457
		if (!empty($sub_template_name))
458
		{
459
			if ($db_show_debug === true)
460
			{
461
				Debug::instance()->add('sub_templates', $sub_template_name);
462
			}
463
464
			// Figure out what the template function is named.
465
			$theme_function = 'template_' . $sub_template_name;
466
467
			if (function_exists($theme_function))
468
			{
469
				try
470
				{
471
					$theme_function();
472
				}
473
				catch (Error $e)
474
				{
475
					$this->templateNotFound($e);
476
				}
477
			}
478
			elseif ($fatal === false)
479
			{
480
				throw new Exception(
481
					'theme_template_error',
482
					'template',
483
					[(string) $sub_template_name]
484
				);
485
			}
486
			elseif ($fatal !== 'ignore')
487
			{
488
				throw new BadFunctionCallException(
489
					Errors::instance()->log_error(
490
						sprintf(
491
							isset($txt['theme_template_error']) ? $txt['theme_template_error'] : 'Unable to load the %s sub template!',
492
							(string) $sub_template_name
493
						),
494
						'template'
495
					)
496
				);
497
			}
498
		}
499
	}
500
}
501