Passed
Push — development ( f9d3d6...da8715 )
by Spuds
01:09 queued 25s
created

Theme   F

Complexity

Total Complexity 90

Size/Duplication

Total Lines 850
Duplicated Lines 0 %

Test Coverage

Coverage 48.08%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 284
dl 0
loc 850
ccs 25
cts 52
cp 0.4808
rs 2
c 3
b 0
f 0
wmc 90

31 Methods

Rating   Name   Duplication   Size   Complexity  
A getLayers() 0 3 1
A getTemplates() 0 3 1
A setRTL() 0 5 1
A __construct() 0 26 1
A getRequestAPI() 0 3 1
A setupHeadersExpiration() 0 9 2
A setupLoggedUserContext() 0 23 3
A setupHeadersContentType() 0 5 1
A template_rawdata() 0 5 1
A setContextCommonStats() 0 20 1
A addInlineJavascript() 0 3 1
A setupGuestContext() 0 17 3
A setupNewsLines() 0 26 4
A addJavascriptVar() 0 3 1
A theme_copyright() 0 15 2
A loadDefaultThemeSettings() 0 10 3
B template_admin_warning_above() 0 39 7
B doScheduledSendMail() 0 31 7
B loadDefaultLayers() 0 50 7
A relativeTimes() 0 28 3
A setContextShowPmPopup() 0 18 4
A cleanHives() 0 21 6
A autoEmbedVideo() 0 28 3
B loadThemeVariant() 0 37 11
A progressiveWebApp() 0 41 2
A themeCss() 0 3 1
A themeJs() 0 3 1
A loadVariant() 0 22 4
A addCodePrettify() 0 16 2
A getScopeFromUrl() 0 5 2
A setContextThemeData() 0 18 3

How to fix   Complexity   

Complex Class

Complex classes like Theme often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Theme, and based on these observations, apply Extract Interface, too.

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