Theme::themeJs()   A
last analyzed

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 0
cp 0
crap 2
rs 10
1
<?php
2
3
/**
4
 * The main abstract theme 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 BBC\ParserWrapper;
17
use ElkArte\Controller\ScheduledTasks;
18
use ElkArte\EventManager;
19
use ElkArte\Helper\FileFunctions;
20
use ElkArte\Helper\HttpReq;
21
use ElkArte\Helper\SiteCombiner;
22
use ElkArte\Helper\Util;
23
use ElkArte\Helper\ValuesContainer;
24
use ElkArte\Http\Headers;
25
use ElkArte\Languages\Txt;
26
use ElkArte\User;
27
28
/**
29
 * Class Theme
30
 */
31
abstract class Theme
32
{
33
	/** @var string */
34
	public const DEFAULT_EXPIRES = 'Mon, 26 Jul 1997 05:00:00 GMT';
35
36
	/** @var int */
37
	public const ALL = -1;
38
39
	/** @var array */
40
	private const CONTENT_TYPES = [
41
		'fatal_error' => 'text/html',
42
		'json' => 'application/json',
43
		'xml' => 'text/xml',
44
		'generic_xml' => 'text/xml'
45
	];
46
47
	/** @var ValuesContainer */
48
	public $user;
49
50
	/** @var HttpReq user input variables */
51
	public $_req;
52
53
	/** @var int The id of the theme being used */
54
	protected $id;
55
56
	/** @var array */
57
	protected $links = [];
58
59
	/** @var string[] Holds base actions that we do not want crawled / indexed */
60
	public $no_index_actions = [];
61
62
	/** @var bool Right to left language support */
63
	protected $rtl;
64
65
	/** @var Templates */
66
	private $templates;
67
68
	/** @var TemplateLayers */
69
	private $layers;
70
71
	/** @var Javascript */
72
	public $javascript;
73
74
	/** @var Css */
75
	public $css;
76
77
	/**
78
	 * Theme constructor.
79
	 *
80
	 * @param int $id
81
	 * @param ValuesContainer $user
82
	 * @param Directories $dirs
83
	 */
84
	public function __construct(int $id, ValuesContainer $user, Directories $dirs)
85
	{
86
		$this->id = $id;
87
		$this->user = $user;
88
		$this->layers = new TemplateLayers();
89
		$this->templates = new Templates($dirs);
90
91
		$this->no_index_actions = [
92
			'profile',
93
			'search',
94
			'calendar',
95
			'memberlist',
96
			'help',
97
			'who',
98
			'stats',
99
			'login',
100
			'reminder',
101
			'register',
102
			'contact'
103
		];
104
105
		$this->_req = HttpReq::instance();
106
107
		// Theme posse
108
		$this->javascript = new Javascript();
109
		$this->css = new Css();
110
	}
111
112 229
	/**
113
	 * The following are expected in the custom Theme.php (or just use the default)
114 229
	 */
115 229
	abstract public function getSettings();
116 229
117 229
	abstract public function template_header();
118
119 229
	abstract public function setupThemeContext();
120 229
121 229
	abstract public function setupCurrentUserContext();
122 229
123
	abstract public function loadCustomCSS();
124 1
125
	abstract public function template_footer();
126
127
	abstract public function loadThemeJavascript();
128
129 229
	/**
130
	 * Get the layers associated with the current theme
131
	 */
132
	public function getLayers(): TemplateLayers
133
	{
134
		return $this->layers;
135
	}
136
137
	/**
138
	 * Get the templates associated with the current theme
139
	 */
140
	public function getTemplates(): Templates
141
	{
142
		return $this->templates;
143 229
	}
144
145
	/**
146
	 * Turn on/off RTL language support
147
	 *
148
	 * @param $toggle
149
	 *
150
	 * @return $this
151
	 */
152
	public function setRTL($toggle): self
153
	{
154
		$this->rtl = (bool) $toggle;
155 279
156
		return $this;
157 279
	}
158
159
	/**
160
	 * Get the value of 'api' from the request
161
	 *
162
	 * What it does:
163 353
	 *  - Retrieves the value of the 'api' parameter from the request.
164
	 *  - Requires that the request was made via AJAX. Validated by checking for
165 353
	 * 'HTTP_X_REQUESTED_WITH' header which much be set in fetch API and/or XMLHttpRequest with
166
	 * setRequestHeader('X-Requested-With', automatically set by jQuery requests.
167
	 *
168
	 * @return string|false The value of the 'api' parameter from the request, trimmed.
169
	 */
170
	public function getRequestAPI(): string|false
171
	{
172
		$api = $this->_req->getRequest('api', 'trim', '');
173
174 231
		return in_array($api, ['xml', 'json', 'html']) && !empty($_SERVER['HTTP_X_REQUESTED_WITH']) ? $api : false;
175
	}
176 231
177
	/**
178
	 * Set the headers expiration
179
	 *
180
	 * What it does:
181 231
	 *  - Sets the Expires and Last-Modified headers in the Headers object.
182
	 *
183 231
	 * @param Headers $header The Headers object to set the headers in.
184
	 */
185 231
	public function setupHeadersExpiration(Headers $header): void
186
	{
187
		global $context;
188
189
		if (empty($context['no_last_modified']))
190
		{
191
			$header
192
				->header('Expires', self::DEFAULT_EXPIRES)
193
				->header('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT');
194
		}
195
	}
196
197
	/**
198
	 * Set up the logged user context
199
	 *
200
	 * What it does:
201
	 *  - Copies relevant user data from the user object to the global context.
202
	 */
203
	public function setupLoggedUserContext(): void
204
	{
205
		global $context;
206
207
		$context['user']['messages'] = $this->user->messages;
0 ignored issues
show
Bug Best Practice introduced by
The property messages does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
208
		$context['user']['unread_messages'] = $this->user->unread_messages;
0 ignored issues
show
Bug Best Practice introduced by
The property unread_messages does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
209
		$context['user']['mentions'] = $this->user->mentions;
0 ignored issues
show
Bug Best Practice introduced by
The property mentions does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
210
211
		// Personal message popup...
212
		$context['user']['popup_messages'] = $this->user->unread_messages > ($_SESSION['unread_messages'] ?? 0);
213
214
		$_SESSION['unread_messages'] = $this->user->unread_messages;
215
216
		$context['user']['avatar'] = [
217
			'href' => empty($this->user->avatar['href']) ? '' : $this->user->avatar['href'],
0 ignored issues
show
Bug Best Practice introduced by
The property avatar does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
218
			'image' => empty($this->user->avatar['image']) ? '' : $this->user->avatar['image'],
219
		];
220
221
		// Figure out how long they've been logged in.
222
		$context['user']['total_time_logged_in'] = [
223
			'days' => floor($this->user->total_time_logged_in / 86400),
0 ignored issues
show
Bug Best Practice introduced by
The property total_time_logged_in does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
224
			'hours' => floor(($this->user->total_time_logged_in % 86400) / 3600),
225
			'minutes' => floor(($this->user->total_time_logged_in % 3600) / 60)
226
		];
227
	}
228
229
	/**
230
	 * Set up guest context
231
	 *
232
	 * What it does:
233
	 *  - Initializes global variables for guest user context
234
	 */
235
	public function setupGuestContext(): void
236
	{
237
		global $modSettings, $context, $txt;
238
239
		$context['user']['messages'] = 0;
240
		$context['user']['unread_messages'] = 0;
241
		$context['user']['mentions'] = 0;
242
		$context['user']['avatar'] = [];
243
		$context['user']['total_time_logged_in'] = ['days' => 0, 'hours' => 0, 'minutes' => 0];
244
		$context['user']['popup_messages'] = false;
245
246
		if (!empty($modSettings['registration_method']) && (int) $modSettings['registration_method'] === 1)
247
		{
248
			$txt['welcome_guest'] .= $txt['welcome_guest_activate'];
249
		}
250
251
		$txt['welcome_guest'] = replaceBasicActionUrl($txt['welcome_guest']);
252
	}
253
254
	/**
255
	 * Set the common stats in the context
256
	 *
257
	 * What it does:
258
	 *  - Sets the total posts, total topics, total members, and latest member stats in the common_stats array of the context
259
	 *  - Sets the formatted string for displaying the total posts in the boardindex_total_posts variable of the context
260
	 */
261
	public function setContextCommonStats(): void
262
	{
263
		global $context, $txt, $modSettings;
264
265 228
		// This looks weird, but it's because BoardIndex.controller.php references the variable.
266
		$href = getUrl('profile', ['action' => 'profile', 'u' => $modSettings['latestMember'], 'name' => $modSettings['latestRealName']]);
267 228
268
		$context['common_stats'] = [
269 228
			'total_posts' => comma_format($modSettings['totalMessages']),
270
			'total_topics' => comma_format($modSettings['totalTopics']),
271 228
			'total_members' => comma_format($modSettings['totalMembers']),
272
			'latest_member' => [
273
				'id' => $modSettings['latestMember'],
274
				'name' => $modSettings['latestRealName'],
275
				'href' => $href,
276
				'link' => '<a href="' . $href . '">' . $modSettings['latestRealName'] . '</a>',
277
			],
278
		];
279
280
		$context['common_stats']['boardindex_total_posts'] = sprintf($txt['boardindex_total_posts'], $context['common_stats']['total_posts'], $context['common_stats']['total_topics'], $context['common_stats']['total_members']);
281
	}
282
283
	/**
284
	 * This is the only template included in the sources.
285
	 */
286
	public function template_rawdata(): void
287
	{
288
		global $context;
289
290
		echo $context['raw_data'];
291
	}
292
293
	/**
294
	 * Set the headers content type
295
	 *
296
	 * What it does:
297
	 *  - Sets the content type of the headers based on the provided context and API.
298
	 *
299
	 * @param Headers $header The Headers instance used to set the content type.
300
	 * @param string $api The API string used to determine the content type.
301
	 */
302
	public function setupHeadersContentType(Headers $header, string $api): void
303
	{
304
		$contentType = self::CONTENT_TYPES[$api] ?? 'text/html';
305
306
		$header->contentType($contentType, 'UTF-8');
307
	}
308
309
	/**
310
	 * Load default theme settings
311
	 *
312
	 * Updates the theme settings by replacing the URL and directory values with the default ones if the 'use_default_images'
313
	 * setting is set to 'defaults' and the 'default_template' setting is provided.
314
	 */
315
	public function loadDefaultThemeSettings(): void
316
	{
317
		global $settings;
318
319
		if (isset($settings['use_default_images'], $settings['default_template'])
320
			&& $settings['use_default_images'] === 'defaults')
321
		{
322
			$settings['theme_url'] = $settings['default_theme_url'];
323
			$settings['images_url'] = $settings['default_images_url'];
324
			$settings['theme_dir'] = $settings['default_theme_dir'];
325
		}
326
	}
327
328
	/**
329
	 * Sets up the news lines for display
330
	 *
331
	 * What it does:
332
	 *  - Retrieves the news lines from the modSettings variable
333
	 *  - Filters out empty lines and trims whitespace
334
	 *  - Parses the news lines using the BBC parser
335
	 *  - Sets a random news line as the 'random_news_line' variable in the context
336
	 *  - Adds the 'news_fader' callback to the 'upper_content_callbacks' array in the context
337
	 *  - Sets the 'show_news' variable in the context based on the 'enable_news' setting in $settings
338
	 */
339
	public function setupNewsLines(): void
340
	{
341
		global $context, $modSettings, $settings;
342
343
		$context['news_lines'] = array_filter(explode("\n", str_replace("\r", '', trim(addslashes($modSettings['news'])))));
344
		$bbc_parser = ParserWrapper::instance();
345
		foreach ($context['news_lines'] as $i => $iValue)
346
		{
347
			if (trim($iValue) === '')
348
			{
349
				continue;
350
			}
351
352
			$context['news_lines'][$i] = $bbc_parser->parseNews(stripslashes(trim($iValue)));
353
		}
354
355
		if (empty($context['news_lines']))
356
		{
357
			return;
358
		}
359
360
		$context['random_news_line'] = $context['news_lines'][mt_rand(0, count($context['news_lines']) - 1)];
361
		$context['upper_content_callbacks'][] = 'news_fader';
362
363
		// This is here because old index templates might still use it.
364
		$context['show_news'] = !empty($settings['enable_news']);
365
	}
366
367
	/**
368
	 * Show the copyright.
369
	 */
370
	public function theme_copyright(): void
371
	{
372
		global $forum_copyright;
373
374
		// Don't display copyright for things like SSI.
375
		if (!defined('FORUM_VERSION'))
376
		{
377
			return;
378
		}
379
380
		// Put in the version...
381
		$forum_copyright = replaceBasicActionUrl(sprintf($forum_copyright, FORUM_VERSION));
382
383
		echo '
384
					', $forum_copyright;
385
	}
386
387
	/**
388
	 * Add a block of inline JavaScript code to be executed later
389
	 *
390
	 * @param string $javascript
391
	 * @param bool $defer = false, define if the script should load in <head> or before the closing <html> tag
392
	 */
393
	public function addInlineJavascript($javascript, $defer = false): void
394
	{
395
		$this->javascript->addInlineJavascript($javascript, $defer);
396
	}
397
398
	/**
399
	 * Add a JavaScript variable for output later (for feeding text strings and similar to JS)
400
	 *
401
	 * @param array $vars array of vars to include in the output done as 'varname' => 'var value'
402
	 * @param bool $escape = false, whether to escape the value
403
	 */
404
	public function addJavascriptVar($vars, $escape = false): void
405
	{
406
		$this->javascript->addJavascriptVar($vars, $escape);
407
	}
408
409
	/**
410
	 * Clean (delete) the hives (cache) for CSS and JS files
411
	 *
412
	 * @param string $type (Optional) The type of hives to clean. Default is 'all'. Possible values are 'all', 'css', 'js'.
413
	 * @return bool Returns true if the hives are successfully cleaned, otherwise false.
414
	 */
415
	public function cleanHives($type = 'all'): bool
416
	{
417
		global $settings;
418
419
		$combiner = new SiteCombiner($settings['default_theme_cache_dir'], $settings['default_theme_cache_url']);
420
		$result = true;
421
422
		if ($type === 'all' || $type === 'css')
423
		{
424
			$result = $combiner->removeCssHives();
425
		}
426
427
		if ($type === 'all' || $type === 'js')
428
		{
429
			$result = $result && $combiner->removeJsHives();
430
		}
431
432
		// Force a cache refresh for the PWA
433
		setPWACacheStale(true);
434
435
		return $result;
436
	}
437
438
	/**
439
	 * If video embedding is enabled, this loads the necessary JS and vars
440
	 */
441
	public function autoEmbedVideo(): void
442
	{
443
		global $txt, $modSettings;
444
445
		if (!empty($modSettings['enableVideoEmbeding']))
446
		{
447
			loadJavascriptFile('elk_jquery_embed.js', ['defer' => true]);
448
449
			$this->addInlineJavascript('
450
				if (typeof oEmbedtext === "undefined") {
451
					var oEmbedtext = ({
452
						embed_limit : ' . (empty($modSettings['video_embed_limit']) ? 25 : $modSettings['video_embed_limit']) . ',
453
						preview_image : ' . JavaScriptEscape($txt['preview_image']) . ',
454
						ctp_video : ' . JavaScriptEscape($txt['ctp_video']) . ',
455
						hide_video : ' . JavaScriptEscape($txt['hide_video']) . ',
456
						youtube : ' . JavaScriptEscape($txt['youtube']) . ',
457
						vimeo : ' . JavaScriptEscape($txt['vimeo']) . ',
458
						dailymotion : ' . JavaScriptEscape($txt['dailymotion']) . ',
459
						tiktok : ' . JavaScriptEscape($txt['tiktok']) . ',
460
						twitter : ' . JavaScriptEscape($txt['twitter']) . ',
461
						facebook : ' . JavaScriptEscape($txt['facebook']) . ',
462
						instagram : ' . JavaScriptEscape($txt['instagram']) . ',
463
					});
464
465
					document.addEventListener("DOMContentLoaded", () => {
466
						if ($.isFunction($.fn.linkifyvideo))
467
						{
468
							$().linkifyvideo(oEmbedtext);
469
						}
470
					});
471
				}
472
			', true);
473
		}
474
	}
475
476
	/**
477
	 * Progressive Web App initialization
478
	 *
479
	 * What it does:
480
	 *  - Sets up the necessary configurations for the Progressive Web App (PWA).
481
	 *  - Adds JavaScript variables, loads necessary JavaScript files, and adds inline JavaScript code.
482
	 *
483
	 * @return void
484
	 */
485
	public function progressiveWebApp(): void
486
	{
487
		global $modSettings, $boardurl, $settings;
488
489
		$this->addJavascriptVar([
490
			'elk_board_url' => JavaScriptEscape($boardurl),
491
		]);
492
		loadJavascriptFile('elk_pwa.js', ['defer' => false]);
493
494
		// Not enabled, let's be sure to remove it should it exist
495
		if (empty($modSettings['pwa_enabled']))
496
		{
497
			$this->addInlineJavascript('
498
				elkPwa().removeServiceWorker();
499
			');
500
501
			return;
502
		}
503
504
		setPWACacheStale();
505
		$theme_scope = $this->getScopeFromUrl($settings['actual_theme_url']);
506
		$default_theme_scope = $this->getScopeFromUrl($settings['default_theme_url']);
507
		$sw_scope = $this->getScopeFromUrl($boardurl);
508
		$this->addInlineJavascript('
509
			document.addEventListener("DOMContentLoaded", function() {
510
				let myOptions = {
511
					swUrl: "elkServiceWorker.js",
512
					swOpt: {
513
						cache_stale: ' . JavaScriptEscape(CACHE_STALE) . ',
514
						cache_id: ' . JavaScriptEscape($modSettings['elk_pwa_cache_stale']) . ',
515
						theme_scope: ' . JavaScriptEscape($theme_scope) . ',
516
						default_theme_scope: ' . JavaScriptEscape($default_theme_scope) . ',
517
						sw_scope: ' . JavaScriptEscape($sw_scope) . ',
518
						nav_preload: 1, // set to 1 to enable, 0 to disable
519
					}
520
				};
521
	
522
				let elkPwaInstance = elkPwa(myOptions);
523
				elkPwaInstance.init();
524
				elkPwaInstance.sendMessage("deleteOldCache", {cache_id: ' . JavaScriptEscape($modSettings['elk_pwa_cache_stale']) . '});
525
				elkPwaInstance.sendMessage("pruneCache");
526
			});'
527
		);
528
	}
529
530
	/**
531
	 * Get the scope from the given URL
532
	 *
533
	 * @param string $url The URL from which to extract the scope
534
	 *
535
	 * @return string The scope extracted from the URL, or the root scope if not found
536
	 */
537
	public function getScopeFromUrl($url): string
538
	{
539
		$parts = parse_url($url);
540
541
		return empty($parts['path']) ? '/' : '/' . trim($parts['path'], '/') . '/';
542
	}
543
544
	/**
545
	 * If the option to pretty output code is on, this loads the JS and CSS
546
	 */
547
	public function addCodePrettify(): void
548
	{
549
		global $modSettings;
550
551
		if (!empty($modSettings['enableCodePrettify']))
552
		{
553
			$this->loadVariant('prettify');
554
			loadJavascriptFile('ext/prettify.min.js', ['defer' => true]);
555
556
			$this->addInlineJavascript('
557
				document.addEventListener("DOMContentLoaded", () => {
558
				if (typeof prettyPrint === "function")
559
				{
560
					prettyPrint();
561
				}
562
			});', true);
563
		}
564
	}
565
566
	/**
567
	 * Load a variant CSS file if found.  Fallback if not, and it exists in this
568
	 * theme's directory
569
	 *
570
	 * @param string $cssFile
571
	 * @param bool $fallBack
572
	 */
573
	public function loadVariant($cssFile, $fallBack = true): void
574
	{
575
		global $settings, $context;
576
577
		$fileFunc = FileFunctions::instance();
578
		if ($fileFunc->fileExists($settings['theme_dir'] . '/css/' . $context['theme_variant'] . '/' . $cssFile . $context['theme_variant'] . '.css'))
579
		{
580
			loadCSSFile($context['theme_variant'] . '/' . $cssFile . $context['theme_variant'] . '.css');
581
			return;
582
		}
583
584
		if (!$fallBack)
585
		{
586
			return;
587
		}
588
589
		if (!$fileFunc->fileExists($settings['theme_dir'] . '/css/' . $cssFile . '.css'))
590
		{
591
			return;
592
		}
593
594
		loadCSSFile($cssFile . '.css');
595
	}
596
597
	/**
598
	 * Relative times require a few variables be set in the JS
599
	 */
600
	public function relativeTimes(): void
601
	{
602
		global $modSettings, $context, $txt;
603
604
		// Relative times?
605
		if (!empty($modSettings['todayMod']) && $modSettings['todayMod'] > 2)
606
		{
607
			loadJavascriptFile('elk_relativeTime.js', ['defer' => true]);
608
			$this->addInlineJavascript('
609
				if (typeof oRttime === "undefined") {
610
					var oRttime = ({
611
						referenceTime : ' . forum_time() * 1000 . ',
612
						now : ' . JavaScriptEscape($txt['rt_now']) . ',
613
						minute : ' . JavaScriptEscape($txt['rt_minute']) . ',
614
						minutes : ' . JavaScriptEscape($txt['rt_minutes']) . ',
615
						hour : ' . JavaScriptEscape($txt['rt_hour']) . ',
616
						hours : ' . JavaScriptEscape($txt['rt_hours']) . ',
617
						day : ' . JavaScriptEscape($txt['rt_day']) . ',
618
						days : ' . JavaScriptEscape($txt['rt_days']) . ',
619
						week : ' . JavaScriptEscape($txt['rt_week']) . ',
620
						weeks : ' . JavaScriptEscape($txt['rt_weeks']) . ',
621
						month : ' . JavaScriptEscape($txt['rt_month']) . ',
622
						months : ' . JavaScriptEscape($txt['rt_months']) . ',
623
						year : ' . JavaScriptEscape($txt['rt_year']) . ',
624
						years : ' . JavaScriptEscape($txt['rt_years']) . ',
625
					});
626
				}
627
				document.addEventListener("DOMContentLoaded", () => {updateRelativeTime();});', true);
628
629
			$context['using_relative_time'] = true;
630
		}
631
	}
632
633
	/**
634
	 * Ensures we kick the mail queue from time to time so that it gets
635
	 * checked as often as possible.
636
	 */
637
	public function doScheduledSendMail(): void
638
	{
639
		global $modSettings;
640
641
		if (!empty(User::$info->possibly_robot))
0 ignored issues
show
Bug Best Practice introduced by
The property possibly_robot does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
642
		{
643
			// @todo Maybe move this somewhere better?!
644
			$controller = new ScheduledTasks(new EventManager());
645
646
			// What to do, what to do?!
647
			if (empty($modSettings['next_task_time']) || $modSettings['next_task_time'] < time())
648
			{
649
				$controller->action_autotask();
650
			}
651
			else
652
			{
653
				$controller->action_reducemailqueue();
654
			}
655
		}
656
		else
657
		{
658
			$type = empty($modSettings['next_task_time']) || $modSettings['next_task_time'] < time() ? 'task' : 'mailq';
659
			$ts = $type === 'mailq' ? $modSettings['mail_next_send'] : $modSettings['next_task_time'];
660
661
			$this->addInlineJavascript('
662
		function elkAutoTask()
663
		{
664
			let tempImage = new Image();
665
			tempImage.src = elk_scripturl + "?scheduled=' . $type . ';ts=' . $ts . '";
666
		}
667
		window.setTimeout("elkAutoTask();", 1);', true);
668
		}
669
	}
670
671
	/**
672
	 * Set the context for showing the PM popup
673
	 *
674
	 * What it does:
675
	 *  - Sets the context variable $context['show_pm_popup'] based on user preferences and current action
676
	 */
677
	public function setContextShowPmPopup(): void
678
	{
679
		global $context, $options, $txt, $scripturl;
680
681
		// This is done to allow theme authors to customize it as they want.
682
		$context['show_pm_popup'] = $context['user']['popup_messages'] && !empty($options['popup_messages']) && $context['current_action'] !== 'pm';
683
684
		// Add the PM popup here instead. Theme authors can still override it simply by editing/removing the 'fPmPopup' in the array.
685
		if ($context['show_pm_popup'])
686
		{
687
			$this->addInlineJavascript('
688
		$(function() {
689
			new elk_Popup({
690
				heading: ' . JavaScriptEscape($txt['show_personal_messages_heading']) . ',
691
				content: ' . JavaScriptEscape(sprintf($txt['show_personal_messages'], $context['user']['unread_messages'], $scripturl . '?action=pm')) . ',
692
				icon: \'i-envelope\'
693
			});
694
		});', true);
695
		}
696
	}
697
698
	/**
699
	 * Set the context theme data
700
	 *
701
	 * What it does:
702
	 *  - Sets the theme data in the context array
703
	 *  - Adds necessary JavaScript variables
704
	 *  - Sets the page title and favicon
705
	 *  - Updates the HTML headers
706
	 */
707
	public function setContextThemeData(): void
708
	{
709
		global $context, $scripturl, $settings, $boardurl, $modSettings, $txt, $mbname;
710
711
		if (empty($settings['theme_version']))
712
		{
713
			$this->addJavascriptVar(['elk_scripturl' => $scripturl], true);
714
		}
715
716
		$this->addJavascriptVar(['elk_forum_action' => getUrlQuery('action', $modSettings['default_forum_action'])], true);
717
718
		$context['page_title'] = $context['page_title'] ?? $mbname;
719
		$context['page_title_html_safe'] = Util::htmlspecialchars(un_htmlspecialchars($context['page_title'])) . (empty($context['current_page']) ? '' : ' - ' . $txt['page'] . (' ' . ($context['current_page'] + 1)));
720
		$context['favicon'] = $boardurl . '/favicon.ico';
721
		$context['apple_touch'] = $boardurl . '/themes/default/images/logos/apple-touch-icon.png';
722
		$context['html_headers'] = $context['html_headers'] ?? '';
723
		$context['theme-color'] = $modSettings['pwa_theme-color'] ?? '#3d6e32';
724
		$context['pwa_manifest_enabled'] = !empty($modSettings['pwa_manifest_enabled']);
725
	}
726
727
	/**
728
	 * If a variant CSS is needed, this loads it
729
	 */
730
	public function loadThemeVariant(): void
731
	{
732
		global $context, $settings, $options;
733
734
		// Overriding - for previews and that ilk.
735
		$variant = $this->_req->getRequest('variant', 'trim', '');
736
		if (!empty($variant))
737
		{
738
			$_SESSION['id_variant'] = $variant;
739
		}
740
741
		// User selection?
742
		if (empty($settings['disable_user_variant']) || allowedTo('admin_forum'))
743
		{
744
			$context['theme_variant'] = empty($_SESSION['id_variant']) ? (!empty($options['theme_variant']) ? $options['theme_variant'] : '') : ($_SESSION['id_variant']);
745
		}
746
747
		// If not a user variant, select the default.
748
		if ($context['theme_variant'] === '' || !in_array($context['theme_variant'], $settings['theme_variants']))
749
		{
750
			$context['theme_variant'] = !empty($settings['default_variant']) && in_array($settings['default_variant'], $settings['theme_variants']) ? $settings['default_variant'] : $settings['theme_variants'][0];
751
		}
752
753
		// Do this to keep things easier in the templates.
754
		$context['theme_variant'] = '_' . $context['theme_variant'];
755
		$context['theme_variant_url'] = $context['theme_variant'] . '/';
756
757
		// The most efficient way of writing multi themes is to use a master index.css plus variant.css files.
758
		if (!empty($context['theme_variant']))
759
		{
760
			loadCSSFile($context['theme_variant'] . '/index' . $context['theme_variant'] . '.css');
761
762
			// Variant icon definitions?
763
			$this->loadVariant('icons_svg', false);
764
765
			// Load a theme variant custom CSS
766
			$this->loadVariant('custom', false);
767
		}
768
	}
769
770
	/**
771
	 * Calls on template_show_error from index.template.php to show warnings
772
	 * and security errors for admins
773
	 */
774
	public function template_admin_warning_above(): void
775
	{
776
		global $context, $txt;
777
778
		if (!empty($context['security_controls_files']))
779
		{
780
			$context['security_controls_files']['type'] = 'serious';
781
			template_show_error('security_controls_files');
782
		}
783
784
		if (!empty($context['security_controls_query']))
785
		{
786
			$context['security_controls_query']['type'] = 'serious';
787
			template_show_error('security_controls_query');
788
		}
789
790
		if (!empty($context['security_controls_ban']))
791
		{
792
			$context['security_controls_ban']['type'] = 'serious';
793
			template_show_error('security_controls_ban');
794
		}
795
796
		if (!empty($context['new_version_updates']))
797
		{
798
			template_show_error('new_version_updates');
799
		}
800
801
		if (!empty($context['accepted_agreement']))
802
		{
803
			template_show_error('accepted_agreement');
804
		}
805
806
		// Any special notices to remind the admin about?
807
		if (!empty($context['warning_controls']))
808
		{
809
			$context['warning_controls']['errors'] = $context['warning_controls'];
810
			$context['warning_controls']['title'] = $txt['admin_warning_title'];
811
			$context['warning_controls']['type'] = 'warning';
812
			template_show_error('warning_controls');
813
		}
814
	}
815
816
	/**
817
	 * Makes the default layers and languages available
818
	 *
819
	 * - Loads index and addon language files as needed
820
	 * - Loads XML, index, or no templates as needed
821
	 * - Loads templates as defined by $settings['theme_templates']
822
	 */
823
	public function loadDefaultLayers(): void
824
	{
825
		global $settings;
826
827
		$simpleActions = [
828
			'quickhelp',
829
			'printpage',
830
			'quotefast',
831
		];
832
833
		call_integration_hook('integrate_simple_actions', [&$simpleActions]);
834
835
		// Output is fully XML and sent by our JavaScript
836
		$api = $this->_req->getRequest('api', 'trim', '');
837
		$valid = !empty($_SERVER['HTTP_X_REQUESTED_WITH']);
838
		$action = $this->_req->getRequest('action', 'trim', '');
839
840
		if ($valid && $api === 'xml')
841
		{
842
			Txt::load('index+Addons');
843
			$this->getLayers()->removeAll();
844
			$this->getTemplates()->load('Xml');
845
		}
846
		// These actions don't require the index template at all.
847
		elseif (in_array($action, $simpleActions, true))
848
		{
849
			Txt::load('index+Addons');
850
			$this->getLayers()->removeAll();
851
		}
852
		else
853
		{
854
			// Custom templates to load, or just default?
855
			$templates = isset($settings['theme_templates']) ? explode(',', $settings['theme_templates']) : ['index'];
856
857
			// Load each template...
858
			foreach ($templates as $template)
859
			{
860
				$this->getTemplates()->load($template);
861
			}
862
863
			// ...and attempt to load their associated language files.
864
			Txt::load(array_merge($templates, ['Addons']), false);
865
866
			// Custom template layers?
867
			$layers = isset($settings['theme_layers']) ? explode(',', $settings['theme_layers']) : ['html', 'body'];
868
869
			$template_layers = $this->getLayers();
870
			$template_layers->setErrorSafeLayers($layers);
871
			foreach ($layers as $layer)
872
			{
873
				$template_layers->addBegin($layer);
874
			}
875
		}
876
	}
877
878
	/**
879
	 * Return the instance of /ElkArte/Themes/Css
880
	 */
881
	public function themeCss(): Css
882
	{
883
		return $this->css;
884
	}
885
886
	/**
887
	 * Return the instance of /ElkArte/Themes/Javascript
888
	 */
889
	public function themeJs(): Javascript
890
	{
891
		return $this->javascript;
892
	}
893
}
894