Passed
Push — development ( e3a728...c7cb34 )
by Spuds
01:05 queued 20s
created

ThemeLoader::setUserPreferences()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 34
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 7
eloc 16
nc 6
nop 0
dl 0
loc 34
ccs 0
cts 13
cp 0
crap 56
rs 8.8333
c 2
b 0
f 0
1
<?php
2
3
/**
4
 * The main ThemeLoader class
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
 * @version 2.0 Beta 1
11
 *
12
 */
13
14
namespace ElkArte\Themes;
15
16
use ElkArte\Cache\Cache;
17
use ElkArte\ext\Composer\Autoload\ClassLoader;
0 ignored issues
show
Bug introduced by
The type ElkArte\ext\Composer\Autoload\ClassLoader 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...
18
use ElkArte\Helper\FileFunctions;
19
use ElkArte\Helper\HttpReq;
20
use ElkArte\Helper\Util;
21
use ElkArte\Helper\ValuesContainer;
22
use ElkArte\Hooks;
23
use ElkArte\Languages\Txt;
24
use ElkArte\Request;
25
use ElkArte\User;
26
use ElkArte\UserInfo;
27
28
/**
29
 * The ThemeLoader class is responsible for loading and initializing themes in ElkArte.
30
 */
31
class ThemeLoader
32
{
33
	/** @var Directories The list of directories. */
34
	protected static $dirs;
35
36
	/** @var ValuesContainer */
37
	public $user;
38
39
	/** @var string[] Theme items we shouldn't be able to change */
40
	protected $immutable_theme_data = [
41
		'actual_theme_url',
42
		'actual_images_url',
43
		'base_theme_dir',
44
		'base_theme_url',
45
		'default_images_url',
46
		'default_theme_dir',
47
		'default_theme_url',
48
		'default_template',
49
		'images_url',
50
		'number_recent_posts',
51
		'smiley_sets_default',
52
		'theme_dir',
53 229
		'theme_id',
54
		'theme_layers',
55 229
		'theme_templates',
56 229
		'theme_url',
57
	];
58 229
59 229
	/** @var Theme The current theme. */
60 229
	private $theme;
61
62 229
	/**
63
	 * Load a theme, by ID.
64
	 *
65
	 * What it does:
66
	 * - identify the theme to be loaded.
67 229
	 * - Checks that the theme is valid and that the user has permission to use it
68 229
	 * - load the users theme settings and site settings into $options.
69
	 * - prepares the list of folders to search for template loading.
70
	 * - sets up $context['user']
71 229
	 * - detects the users browser and sets a mobile-friendly environment if needed
72
	 * - loads default JS variables for use in every theme
73
	 * - loads default JS scripts for use in every theme
74
	 *
75
	 * @param int $id = 0
76
	 * @param bool $initialize = true
77
	 */
78
	public function __construct(private $id = 0, $initialize = true)
79
	{
80
		global $context;
81
82
		$this->user = User::$info;
83 229
84
		$this->initTheme();
85
		if (!$initialize)
86 229
		{
87
			return;
88 82
		}
89
90
		$this->loadThemeUrls();
91
92
		// Load various user and server values into context.
93
		loadUserContext();
94
		detectServer();
95
96
		// Fetch/Set theme and min-max window preferences
97
		$this->setAdminPreferences();
98
		$this->setUserPreferences();
99
100
		$this->setupContext();
101
		$this->loadThemeSettings();
102
		$this->loadThemeVariantAndCSS();
103
		$this->processAgreements();
104
105 82
		$this->theme->loadThemeJavascript();
106
		$this->callIntegrationHooks();
107
108
		$context['theme_loaded'] = true;
109 147
	}
110
111
	/**
112
	 * Initialize a theme for use
113
	 */
114 229
	private function initTheme(): void
115
	{
116
		global $settings, $options, $context;
117 229
118 229
		// Validate / fetch the themes id
119
		$this->getThemeId();
120
121 229
		// Need to know who we are loading the theme for
122
		$member = empty($this->user->id) ? -1 : $this->user->id;
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
123 147
124 147
		// Load in the theme variables for them
125 147
		$themeData = $this->getThemeData($member);
126
		$settings = $themeData[0];
127
		$options = $themeData[$member];
128
129 229
		$settings['theme_id'] = $this->id;
130
		$settings['actual_theme_url'] = $settings['theme_url'];
131
		$settings['actual_images_url'] = $settings['images_url'];
132 229
		$settings['actual_theme_dir'] = $settings['theme_dir'];
133
134
		// Set the name of the default theme to something PHP will recognize.
135 229
		$themeName = basename($settings['theme_dir']) === 'default'
136 229
			? 'DefaultTheme'
137 229
			: ucfirst(basename($settings['theme_dir']));
138
139
		$loader = new ClassLoader();
140
		$loader->setPsr4('\\ElkArte\\Themes\\' . $themeName . '\\', $themeData[0]['default_theme_dir']);
141 229
		$loader->register();
142
143
		// Set up the theme file.
144
		require_once($settings['theme_dir'] . '/Theme.php');
145
		$class = '\\ElkArte\\Themes\\' . $themeName . '\\Theme';
146
147
		static::$dirs = new Directories($settings);
148
		User::$info = User::$info ?? new UserInfo([]);
149
150
		// Initialize Theme.php from the default or if it exists from the custom theme
151
		$this->theme = new $class($this->id, User::$info, static::$dirs);
152
		$context['theme_instance'] = $this->theme;
153
	}
154 229
155
	/**
156 1
	 * Resolves the ID of a theme.
157
	 *
158
	 * The identifier can be specified in:
159
	 *  - A GET variable if theme selection is enabled
160 229
	 *  - The session
161
	 *  - User's preferences
162
	 *  - Board
163 229
	 *  - Forum default
164
	 *
165
	 * In addition, the ID is verified against a comma-separated list of
166 229
	 * known good themes. This check is skipped if the user is an admin.
167
	 *
168
	 * @return void Theme ID to load
169 229
	 */
170
	private function getThemeId(): void
171
	{
172 229
		global $modSettings, $board_info;
173
174 1
		// The user has selected a theme
175
		if (!empty($modSettings['theme_allow']) || allowedTo('admin_forum'))
176
		{
177
			$this->_chooseTheme();
178 229
		}
179
		// The board specified the theme.
180
		elseif (!empty($board_info['theme']))
181
		{
182
			$this->id = $board_info['theme'];
183
		}
184 229
		// The theme is the forum's default.
185
		else
186 229
		{
187
			$this->id = $modSettings['theme_guests'];
188
		}
189
190 229
		// Whatever we found, make sure it's valid
191 229
		$this->_validThemeID();
192
	}
193
194 229
	/**
195
	 * Sets the chosen theme id
196 228
	 */
197
	private function _chooseTheme(): void
198
	{
199 229
		$_req = HttpReq::instance();
200
201
		// The (ACP) previously set the theme
202
		if (!empty($this->id) && !empty($_req->isSet('th')))
203
		{
204
			return;
205
		}
206
207 229
		// The theme was specified by Get or Post.
208
		if (!empty($_req->getRequest('theme', 'intval')))
209
		{
210 229
			$this->id = $_req->get('theme');
211
			$_SESSION['theme'] = $this->id;
212
		}
213
		// REQUEST specified the theme... previously.
214
		elseif (!empty($_req->getSession('theme')))
215 229
		{
216
			$this->id = (int) $_req->getSession('theme');
217
		}
218
		// The theme is just the user's choice. (might use ?board=1;theme=0 to force board theme.)
219
		elseif (!empty($this->user->theme))
0 ignored issues
show
Bug Best Practice introduced by
The property theme does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
220 229
		{
221
			$this->id = $this->user->theme;
222 229
		}
223 229
	}
224 229
225
	/**
226
	 * Validates, and corrects for error, that the theme id is capable of being used.
227
	 */
228 229
	private function _validThemeID(): void
229
	{
230
		global $modSettings, $ssi_theme;
231
232
		// Ensure that the theme is known... no foul play.
233
		if (!allowedTo('admin_forum'))
234
		{
235
			$themes = explode(',', $modSettings['knownThemes']);
236
			if ((!empty($ssi_theme) && $this->id !== (int) $ssi_theme)
237
				|| !in_array($this->id, $themes, true))
238 229
			{
239
				$this->id = $modSettings['theme_guests'];
240
			}
241
		}
242
	}
243
244
	/**
245
	 * Load in the theme variables for a given theme / member combination
246
	 *
247
	 * @param int $member
248 229
	 *
249
	 * @return array
250 229
	 */
251
	private function getThemeData($member): array
252
	{
253 229
		global $modSettings, $boardurl;
254
255
		$cache = Cache::instance();
256 229
257
		// Do we already have this members theme data and specific options loaded (for aggressive cache settings)?
258
		$temp = [];
259 229
		if ($cache->levelHigherThan(1)
260 229
			&& $cache->getVar($temp, 'theme_settings-' . $this->id . ':' . $member, 60)
261
			&& time() - 60 > $modSettings['settings_updated'])
262
		{
263
			$themeData = $temp;
264
			$flag = true;
265 229
		}
266
		// Or do we just have the system-wide theme settings cached?
267 229
		elseif ($cache->getVar($temp, 'theme_settings-' . $this->id, 90)
268
			&& time() - 60 > $modSettings['settings_updated'])
269
		{
270 229
			$themeData = $temp + [$member => []];
271
		}
272
		// Nothing at all then
273 229
		else
274
		{
275
			$themeData = [-1 => [], 0 => [], $member => []];
276 229
		}
277 229
278 229
		if (empty($flag))
279
		{
280 229
			$db = database();
281 229
282 229
			$immutable_theme_data = $this->immutable_theme_data;
283 229
284
			// Load variables from the current or default theme, global or this user's.
285
			$db->fetchQuery('
286 229
			SELECT 
287 229
				variable, value, id_member, id_theme
288 229
			FROM {db_prefix}themes
289
			WHERE id_member' . (empty($themeData[0]) ? ' IN (-1, 0, {int:id_member})' : ' = {int:id_member}') . '
290
				AND id_theme' . ($this->id === 1 ? ' = {int:id_theme}' : ' IN ({int:id_theme}, 1)'),
291 229
				[
292 229
					'id_theme' => $this->id,
293 229
					'id_member' => $member,
294 229
				]
295
			)->fetch_callback(
296
				static function ($row) use ($immutable_theme_data, &$themeData) {
297 229
					// There are just things we shouldn't be able to change as members.
298 229
					if ((int) $row['id_member'] !== 0 && in_array($row['variable'], $immutable_theme_data, true))
299 229
					{
300 229
						return;
301
					}
302
303 229
					// If this is the theme_dir of the default theme, store it.
304 229
					if ((int) $row['id_theme'] === 1 && empty($row['id_member'])
305
						&& in_array($row['variable'], ['theme_dir', 'theme_url', 'images_url']))
306
					{
307
						$themeData[0]['default_' . $row['variable']] = $row['value'];
308
					}
309
310
					// If this isn't set yet, is a theme option, or is not the default theme.
311
					if (!isset($themeData[$row['id_member']][$row['variable']]) || (int) $row['id_theme'] !== 1)
312
					{
313
						$themeData[$row['id_member']][$row['variable']] = str_starts_with($row['variable'], 'show_') ? (int) $row['value'] === 1 : $row['value'];
314
					}
315
				}
316
			);
317
318
			$fileFunctions = FileFunctions::instance();
319
			if ($fileFunctions->fileExists($themeData[0]['default_theme_dir'] . '/cache')
320
				&& $fileFunctions->isWritable($themeData[0]['default_theme_dir'] . '/cache'))
321 229
			{
322
				$themeData[0]['default_theme_cache_dir'] = $themeData[0]['default_theme_dir'] . '/cache';
323 229
				$themeData[0]['default_theme_cache_url'] = $themeData[0]['default_theme_url'] . '/cache';
324
			}
325
			else
326 229
			{
327
				$themeData[0]['default_theme_cache_dir'] = CACHEDIR;
328 229
				$themeData[0]['default_theme_cache_url'] = $boardurl . '/cache';
329
			}
330
331
			// Set the defaults if the user has not chosen on their own
332
			if (!empty($themeData[-1]))
333
			{
334
				foreach ($themeData[-1] as $k => $v)
335
				{
336
					if (!isset($themeData[$member][$k]))
337
					{
338
						$themeData[$member][$k] = $v;
339
					}
340
				}
341
			}
342 229
343 229
			// If being aggressive, we save the site wide and member theme settings
344
			if ($cache->levelHigherThan(1))
345
			{
346
				$cache->put('theme_settings-' . $this->id . ':' . $member, $themeData, 60);
347
			}
348 229
			// Only if we didn't already load that part of the cache...
349
			elseif (!isset($temp))
350 229
			{
351
				$cache->put('theme_settings-' . $this->id, [-1 => $themeData[-1], 0 => $themeData[0]], 90);
352
			}
353 229
		}
354
355
		return $themeData;
356
	}
357
358 229
	/**
359
	 * Detects url and checks against expected boardurl
360
	 *
361
	 * Attempts to correct improper URL's
362
	 *  - Point to httpS if http is requested
363
	 *  - Point to www.siteName.com if siteName.com is requested
364 229
	 *  - Point to a proper address if this is an alias address
365
	 */
366
	private function loadThemeUrls(): void
367
	{
368
		global $scripturl, $boardurl;
369 229
370
		// Check to see if they're accessing it from the wrong place.
371
		if (isset($_SERVER['HTTP_HOST']) || isset($_SERVER['SERVER_NAME']))
372
		{
373 229
			$detected_url = detectServer()->supportsSSL() ? 'https://' : 'http://';
374
			$detected_url .= detectServer()->getHost();
375
376
			$temp = preg_replace('~/' . preg_quote(basename($scripturl), '~') . '(/.+)?$~', '', str_replace('\\', '/', dirname($_SERVER['PHP_SELF'])));
377
			$detected_url .= ($temp !== '/') ? $temp : '';
378
		}
379 229
380
		if (isset($detected_url) && $detected_url !== $boardurl)
381 229
		{
382
			// Try #1 - check if it's in a list of alias addresses.
383
			$do_fix = $this->checkAlias($detected_url);
384 229
385
			// Hmm... check #2 - is it just different from a www?  Send them to the correct place!!
386 9
			if ($do_fix === false)
387 9
			{
388
				$this->checkWWWRedirect($detected_url);
389 9
			}
390
391
			// #3 is just a check for SSL...
392 229
			if (str_replace('https://', 'http://', $detected_url) === $boardurl)
393
			{
394
				$do_fix = true;
395
			}
396
397
			// Okay, #4 - perhaps it's an IP address?  We're going to want to use that one, then. (assuming it's the IP or something...)
398
			// Okay, #4a - The previous code had || preg_match('~^http[s]?://(?:[\d\.:]+|\[[\d:]+\](?::\d+)?)(?:$|/)~', $detected_url) === 1)
399
			// however, this has shown to replace valid site name with an ip address, leaving the site wrecked due to $_SERVER
400
			// returning suspect info
401
			if ($do_fix === true)
402 229
			{
403
				$this->fixThemeUrls($detected_url);
404 229
			}
405
		}
406 229
	}
407
408
	/**
409 229
	 * Checks if the detected URL needs to be redirected to its www counterpart.
410 229
	 *
411 229
	 * @param string $detected_url The detected URL to check for redirection.
412 229
	 */
413
	private function checkWWWRedirect($detected_url): void
414
	{
415
		global $boardurl;
416
417
		$detected_url = str_replace('://', '://www.', $detected_url);
418
		if ($detected_url !== $boardurl)
419 229
		{
420 229
			return;
421
		}
422
423
		if (ELK === 'SSI')
0 ignored issues
show
introduced by
The condition ElkArte\Themes\ELK === 'SSI' is always true.
Loading history...
424
		{
425
			return;
426
		}
427
428 229
		if (!empty($_GET) && count($_GET) !== 1)
429
		{
430
			return;
431 229
		}
432
433 229
		// Okay, this seems weird, but we don't want an endless loop - this will make $_GET not empty ;).
434
		if (empty($_GET))
435
		{
436 229
			redirectexit('wwwRedirect');
437
		}
438
		elseif (key($_GET) !== 'wwwRedirect')
439
		{
440
			redirectexit('wwwRedirect;' . key($_GET) . '=' . current($_GET));
441
		}
442
	}
443
444
	/**
445
	 * Checks if the provided URL matches any of the forum alias URLs.
446
	 *
447
	 * @param string $detected_url The detected URL to check against the forum alias URLs.
448
	 *
449
	 * @return bool Returns true if the provided URL matches any of the forum alias URLs, otherwise false.
450
	 */
451
	private function checkAlias($detected_url): bool
452
	{
453
		global $modSettings;
454
455 229
		$do_fix = false;
456
		if (!empty($modSettings['forum_alias_urls']))
457
		{
458
			$aliases = explode(',', $modSettings['forum_alias_urls']);
459 229
			foreach ($aliases as $alias)
460 229
			{
461
				// Rip off all the boring parts, spaces, etc.
462 229
				$alias = trim($alias);
463 229
				if ($detected_url === $alias || strtr($detected_url, ['http://' => '', 'https://' => '']) === $alias)
464
				{
465 229
					$do_fix = true;
466
				}
467
			}
468 229
		}
469
470
		return $do_fix;
471
	}
472
473
	/**
474 229
	 * Called if the detected URL is different from boardurl but is a common
475 229
	 * variation in which case it updates key system variables so it works.
476
	 *
477 229
	 * @param string $detected_url
478
	 */
479
	private function fixThemeUrls($detected_url): void
480
	{
481 229
		global $boardurl, $scripturl, $settings, $modSettings, $context, $board_info;
482
483 229
		// Caching is good ;).
484
		$oldurl = $boardurl;
485 229
486
		// Fix $boardurl and $scripturl.
487
		$boardurl = $detected_url;
488 229
		$scripturl = strtr($scripturl, [$oldurl => $boardurl]);
489 229
		$_SERVER['REQUEST_URL'] = strtr($_SERVER['REQUEST_URL'], [$oldurl => $boardurl]);
490
491 229
		// Fix the theme urls...
492 229
		$settings['theme_url'] = strtr($settings['theme_url'], [$oldurl => $boardurl]);
493
		$settings['default_theme_url'] = strtr($settings['default_theme_url'], [$oldurl => $boardurl]);
494
		$settings['actual_theme_url'] = strtr($settings['actual_theme_url'], [$oldurl => $boardurl]);
495
		$settings['images_url'] = strtr($settings['images_url'], [$oldurl => $boardurl]);
496
		$settings['default_images_url'] = strtr($settings['default_images_url'], [$oldurl => $boardurl]);
497
		$settings['actual_images_url'] = strtr($settings['actual_images_url'], [$oldurl => $boardurl]);
498
499
		// And just a few mod settings :).
500
		$modSettings['smileys_url'] = strtr($modSettings['smileys_url'], [$oldurl => $boardurl]);
501 229
		$modSettings['avatar_url'] = strtr($modSettings['avatar_url'], [$oldurl => $boardurl]);
502
503 229
		// Clean up after loadBoard().
504
		if (isset($board_info['moderators']))
505 229
		{
506
			foreach ($board_info['moderators'] as $k => $dummy)
507 155
			{
508
				$board_info['moderators'][$k]['href'] = strtr($dummy['href'], [$oldurl => $boardurl]);
509
				$board_info['moderators'][$k]['link'] = strtr($dummy['link'], ['"' . $oldurl => '"' . $boardurl]);
510
			}
511
		}
512
513 229
		foreach ($context['breadcrumbs'] as $k => $dummy)
514
		{
515
			$context['breadcrumbs'][$k]['url'] = strtr($dummy['url'], [$oldurl => $boardurl]);
516
		}
517
	}
518 229
519
	/**
520 229
	 * Sets the admin preferences for the current user.
521
	 */
522
	private function setAdminPreferences(): void
523
	{
524 229
		global $context, $options;
525
526
		$context['admin_preferences'] = [];
527
		// Update the option.
528
		if (!isset($this->user) || $this->user->is_guest === true)
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...
529
		{
530
			return;
531
		}
532 229
		if (empty($options['admin_preferences']))
533
		{
534 229
			return;
535
		}
536
537 229
		$context['admin_preferences'] = serializeToJson($options['admin_preferences'], static function ($array_form) {
538
			global $context;
539
540
			require_once(SUBSDIR . '/Admin.subs.php');
541
542
			// Required for updateAdminPreferences to run
543
			$context['admin_preferences'] = $array_form;
544
			updateAdminPreferences();
545
		});
546
	}
547
548
	/**
549 229
	 * Sets user preferences.
550
	 *
551
	 * Updates the `minmax_preferences` option with the user's preferences if the user is not a guest and if the `minmax_preferences` option is not empty.
552
	 *
553
	 * If the user is a guest and the `upshrink` cookie is set, the `minmax_preferences` array is set with the `upshrink` cookie value to prevent collapse jumping.
554
	 */
555
	private function setUserPreferences(): void
556
	{
557
		global $context, $options;
558
559
		$context['minmax_preferences'] = [];
560
561
		// Update the option.
562
		if ((isset($this->user) && $this->user->is_guest === false) && !empty($options['minmax_preferences']))
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...
563
		{
564
			$context['minmax_preferences'] = serializeToJson($options['minmax_preferences'], static function ($array_form) {
565
				global $settings;
566
567
				require_once(SUBSDIR . '/Themes.subs.php');
568
				updateThemeOptions([
569
					$settings['theme_id'],
570
					User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
571
					'minmax_preferences',
572
					json_encode($array_form),
573
				]);
574
			});
575
		}
576
577
		// Guest may have collapsed the header, check the cookie to prevent collapse jumping
578
		if (!isset($this->user) || !$this->user->is_guest)
579
		{
580
			return;
581
		}
582
583
		if (!isset($_COOKIE['upshrink']))
584
		{
585
			return;
586
		}
587
588
		$context['minmax_preferences'] = ['upshrink' => $_COOKIE['upshrink']];
589
	}
590
591
	/**
592
	 * Set up the context with necessary data.
593
	 */
594
	private function setupContext(): void
595
	{
596
		global $mbname, $context, $scripturl, $modSettings, $txt;
597
598
		$this->loadThemeContext();
599 229
600
		// @todo These really don't belong here since they are more general than the theme.
601
		$context['forum_name'] = $mbname;
602
		$context['forum_name_html_safe'] = $context['forum_name'];
603
604
		// Showing the login bar?
605
		if ($this->isGuestShowLoginBar())
606
		{
607
			$this->showLoginBar();
608
		}
609
610
		// Set the top level breadcrumbs.
611
		array_unshift($context['breadcrumbs'], [
612
			'url' => $scripturl,
613
			'name' => $context['forum_name'],
614
		]);
615
616
		// Just some mobile-friendly settings
617
		if (strpos(Request::instance()->user_agent(), 'Mobi'))
618
		{
619
			// Disable the search dropdown.
620
			$modSettings['search_dropdown'] = false;
621
		}
622
623
		// Guests may still need a name.
624
		if ($context['user']['is_guest'] && empty($context['user']['name']))
625
		{
626
			$context['user']['name'] = $txt['guest_title'];
627
		}
628
629
		// Set the new feed links for use in the template
630
		if (empty($modSettings['xmlnews_enable']))
631
		{
632
			return;
633
		}
634
635
		if (empty($modSettings['allow_guestAccess']) && !$context['user']['is_logged'])
636
		{
637
			return;
638
		}
639
640
		$context['newsfeed_urls'] = [
641
			'rss' => getUrl('action', ['action' => '.xml', 'type' => 'rss2', 'limit' => (empty($modSettings['xmlnews_limit']) ? 5 : $modSettings['xmlnews_limit'])]),
642
			'atom' => getUrl('action', ['action' => '.xml', 'type' => 'atom', 'limit' => (empty($modSettings['xmlnews_limit']) ? 5 : $modSettings['xmlnews_limit'])]),
643
		];
644
	}
645
646
	/**
647
	 * Loads various theme-related settings into context and sets system-wide theme defaults
648
	 */
649
	private function loadThemeContext(): void
650 229
	{
651
		global $context, $settings, $modSettings, $txt;
652 229
653
		// Some basic information...
654
		$init = [
655
			'html_headers' => '',
656 229
			'links' => [],
657
			'css_files' => [],
658
			'javascript_files' => [],
659
			'css_rules' => [],
660
			'javascript_inline' => ['standard' => [], 'defer' => []],
661
			'javascript_vars' => [],
662
		];
663
		foreach ($init as $area => $value)
664 229
		{
665
			$context[$area] = $context[$area] ?? $value;
666 229
		}
667
668
		// Set a couple of bits for the template.
669
		$context['right_to_left'] = !empty($txt['lang_rtl']);
670 229
		$context['tabindex'] = 1;
671 229
672
		$context['theme_variant'] = '';
673 229
		$context['theme_variant_url'] = '';
674 229
675
		$context['menu_separator'] = empty($settings['use_image_buttons']) ? ' | ' : ' ';
676 229
		$context['can_register'] = empty($modSettings['registration_method']) || (int) $modSettings['registration_method'] !== 3;
677 229
678 229
		foreach (['theme_header', 'upper_content'] as $call)
679
		{
680 229
			if (!isset($context[$call . '_callbacks']))
681
			{
682 229
				$context[$call . '_callbacks'] = [];
683
			}
684 115
		}
685
686
		// This allows sticking some HTML on the page output - useful for controls.
687
		$context['insert_after_template'] = '';
688
	}
689 229
690 229
	/**
691
	 * Determines whether to show the login bar for guest users.
692
	 *
693
	 * @return bool Returns `true` if the login bar should be shown for guest users, otherwise `false`.
694
	 */
695
	private function isGuestShowLoginBar(): bool
696
	{
697
		global $modSettings;
698
699
		return (!isset($this->user) || $this->user->is_guest) && $modSettings['enableVBStyleLogin'];
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...
700
	}
701
702
	/**
703
	 * Sets up the login bar.
704
	 */
705
	private function showLoginBar(): void
706
	{
707
		global $context;
708
709
		$context['show_login_bar'] = true;
710
		$context['theme_header_callbacks'][] = 'login_bar';
711
	}
712
713
	/**
714
	 * Loads the theme settings.
715
	 *
716
	 * This method initializes the theme settings and loads the basic layers.
717
	 */
718
	private function loadThemeSettings(): void
719
	{
720
		global $modSettings, $settings, $txt;
721
722
		// Defaults in case of odd things
723
		$settings['avatars_on_indexes'] = 0;
724
725
		// Initialize the theme.
726
		$settings = array_merge($settings, $this->theme->getSettings());
727
728
		// Load the basic layers
729
		$this->theme->loadDefaultLayers();
730
731
		// Call initialization theme integration functions.
732
		call_integration_hook('integrate_init_theme', [$this->id, &$settings]);
733
734
		// Any theme-related strings that need to be loaded?
735
		if (!empty($settings['require_theme_strings']))
736
		{
737
			Txt::load('ThemeStrings', false);
738
		}
739
740
		// Allow overriding the board wide time/number formats.
741
		if (empty(User::$settings['time_format']) && !empty($modSettings['time_format']))
742
		{
743
			$this->user->time_format = $modSettings['time_format'];
0 ignored issues
show
Bug Best Practice introduced by
The property time_format does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __set, consider adding a @property annotation.
Loading history...
744
		}
745
746
		if (isset($settings['use_default_images']) && $settings['use_default_images'] === 'always')
747
		{
748
			$settings['theme_url'] = $settings['default_theme_url'];
749
			$settings['images_url'] = $settings['default_images_url'];
750
			$settings['theme_dir'] = $settings['default_theme_dir'];
751
		}
752
753
		// Make a special URL for the language.
754
		$settings['lang_images_url'] = $settings['images_url'] . '/' . (empty($txt['image_lang']) ? $this->user->language : $txt['image_lang']);
0 ignored issues
show
Bug Best Practice introduced by
The property language does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
755
	}
756
757
	/**
758
	 * Load the theme variant and CSS files.
759
	 *
760
	 * What it does:
761
	 * - Loads the icon SVG support file with fallback to the default theme.
762
	 * - Loads the theme variant if it exists.
763
	 * - Sets up the header logo URL.
764
	 * - Loads RTL (right-to-left) CSS file for RTL languages.
765
	 * - Loads RTL theme variant CSS file for RTL languages and if a theme variant is defined.
766
	 */
767
	private function loadThemeVariantAndCSS(): void
768
	{
769
		global $context, $settings;
770
771
		// Load the icon SVG support file with fallback to default theme
772
		loadCSSFile('icons_svg.css');
773
774
		// We allow theme variants because we're cool.
775
		if (!empty($settings['theme_variants']))
776
		{
777
			$this->theme->loadThemeVariant();
778
		}
779
780
		// A bit lonely maybe, though I think it should be set up *after* the theme variants detection
781
		$context['header_logo_url_html_safe'] = empty($settings['header_logo_url'])
782
			? $settings['images_url'] . '/' . $context['theme_variant_url'] . 'logo_elk.png'
783
			: Util::htmlspecialchars($settings['header_logo_url']);
784
785
		// RTL languages require an additional stylesheet.
786
		if ($context['right_to_left'])
787
		{
788
			loadCSSFile('rtl.css');
789
		}
790
791
		if (empty($context['theme_variant']))
792
		{
793
			return;
794
		}
795
796
		if (!$context['right_to_left'])
797
		{
798
			return;
799
		}
800
801
		loadCSSFile($context['theme_variant'] . '/rtl' . $context['theme_variant'] . '.css');
802
	}
803
804
	/**
805
	 * Process the agreements and update the context accordingly.
806
	 *
807
	 * What it does:
808
	 * - Clears the session variables for agreement acceptance.
809
	 * - Sets the appropriate error messages in the context if an agreement has been accepted.
810
	 */
811
	public function processAgreements(): void
812
	{
813
		global $context, $txt;
814
815
		if (!empty($_SESSION['agreement_accepted']))
816
		{
817
			$_SESSION['agreement_accepted'] = null;
818
			$context['accepted_agreement'] = [
819
				'errors' => [
820
					'accepted_agreement' => $txt['agreement_accepted']
821
				]
822
			];
823
		}
824
825
		if (!empty($_SESSION['privacypolicy_accepted']))
826
		{
827
			$_SESSION['privacypolicy_accepted'] = null;
828
			$context['accepted_agreement'] = [
829
				'errors' => [
830
					'accepted_privacy_policy' => $txt['privacypolicy_accepted']
831
				]
832
			];
833
		}
834
	}
835
836
	/**
837
	 * Calls the integration hooks related to the theme.
838
	 *
839
	 * - Sets the theme directory path for integration hooks.
840
	 * - Includes any additional files specified by the 'integrate_theme_include' hook.
841
	 * - Calls the 'integrate_load_theme' hook to load theme integration functions.
842
	 */
843
	private function callIntegrationHooks(): void
844
	{
845
		global $settings;
846
847
		Hooks::instance()->newPath(['$themedir' => $settings['theme_dir']]);
848
849
		// Any files to include at this point?
850
		call_integration_include_hook('integrate_theme_include');
851
852
		// Call load theme integration functions.
853
		call_integration_hook('integrate_load_theme');
854
	}
855
856
	/**
857
	 * This loads the bare minimum data.
858
	 *
859
	 * - Needed by scheduled tasks.
860
	 * - Needed by any other code that needs language files before the forum (the theme) is loaded.
861
	 */
862
	public static function loadEssentialThemeData(): void
863
	{
864
		global $settings, $modSettings, $mbname, $context;
865
866
		if (!function_exists('database'))
867
		{
868
			throw new \Exception('');
869
		}
870
871
		$db = database();
872
873
		// Get all the default theme variables.
874
		$db->fetchQuery('
875
			SELECT 
876
				id_theme, variable, value
877
			FROM {db_prefix}themes
878
			WHERE id_member = {int:no_member}
879
				AND id_theme IN (1, {int:theme_guests})',
880
			[
881
				'no_member' => 0,
882
				'theme_guests' => $modSettings['theme_guests'],
883
			]
884
		)->fetch_callback(
885
			static function ($row) {
886
				global $settings;
887
888
				$settings[$row['variable']] = $row['value'];
889
				$indexes_to_default = [
890
					'theme_dir',
891
					'theme_url',
892
					'images_url',
893
				];
894
895
				// Is this the default theme?
896
				if ($row['id_theme'] !== '1')
897
				{
898
					return;
899
				}
900
901
				if (!in_array($row['variable'], $indexes_to_default, true))
902
				{
903
					return;
904
				}
905
906
				$settings['default_' . $row['variable']] = $row['value'];
907
			}
908
		);
909
910
		static::$dirs = new Directories($settings);
911
912
		// Check we have some directories' setup.
913
		if (static::$dirs->hasDirectories() === false)
914
		{
915
			static::$dirs->reloadDirectories($settings);
916
		}
917
918
		// Assume we want this.
919
		$context['forum_name'] = $mbname;
920
		$context['forum_name_html_safe'] = $context['forum_name'];
921
922
		static::loadLanguageFile('index+Addons');
923
	}
924
925
	/**
926
	 * Load a language file.
927
	 *
928
	 * - Tries the current and default themes as well as the user and global languages.
929
	 *
930
	 * @param string $template_name
931
	 * @param string $lang = ''
932
	 * @param bool $fatal = true
933
	 * @param bool $force_reload = false
934
	 *
935
	 * @return string The language actually loaded.
936
	 */
937
	public static function loadLanguageFile(
938
		$template_name,
939
		$lang = '',
940
		$fatal = false, // @todo reset to true when appropriate
941
		$force_reload = false
942
	): string
943
	{
944
		return static::loadLanguageFiles(
945
			explode('+', $template_name),
946
			$lang,
947
			$fatal,
948
			$force_reload
949
		);
950
	}
951
952
	/**
953
	 * Load a language file.
954
	 *
955
	 * - Tries the current and default themes as well as the user and global languages.
956
	 *
957
	 * @param string[] $template_name
958
	 * @param string $lang = ''
959
	 * @param bool $fatal = true
960
	 * @param bool $force_reload = false
961
	 *
962
	 * @return string The language actually loaded.
963
	 */
964
	public static function loadLanguageFiles(array $template_name, $lang = '', $fatal = true, $force_reload = false): string
965
	{
966
		// Needed by the loaded files
967
		global $language, $settings, $modSettings, $db_show_debug, $txt;
968
		static $already_loaded = [];
969
970
		// For each file open it up and write it out!
971
		foreach ($template_name as $template)
972
		{
973
			$fix_arrays = $template === 'index';
974
975
			Txt::load($template, true, $fix_arrays);
976
		}
977
978
		// Return the language actually loaded.
979
		return $lang;
980
	}
981
982
	/**
983
	 * @return Theme the current theme
984
	 */
985
	public function getTheme(): Theme
986
	{
987
		return $this->theme;
988
	}
989
}
990