Passed
Push — development ( b93807...dda237 )
by Emanuele
01:10 queued 23s
created

Theme::template_header()   B

Complexity

Conditions 8
Paths 16

Size

Total Lines 41
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 20
nc 16
nop 0
dl 0
loc 41
ccs 0
cts 17
cp 0
crap 72
rs 8.4444
c 1
b 0
f 0
1
<?php
2
3
/**
4
 * The default theme
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 dev
14
 *
15
 */
16
17
namespace ElkArte\Themes\DefaultTheme;
18
19
use BBC\ParserWrapper;
20
use ElkArte\Cache\Cache;
21
use ElkArte\Controller\ScheduledTasks;
22
use ElkArte\EventManager;
23
use ElkArte\Http\Headers;
24
use ElkArte\SiteCombiner;
25
use ElkArte\Themes\Theme as BaseTheme;
26
use ElkArte\Themes\ThemeLoader;
27
use ElkArte\Util;
28
29
/**
30
 * Class Theme
31
 *
32
 * - Extends the abstract theme class
33
 *
34
 * @package Themes\DefaultTheme
35
 */
36
class Theme extends BaseTheme
37
{
38
	/**
39
	 * Initialize the template... mainly little settings.
40
	 *
41 1
	 * @return array Theme settings
42
	 */
43
	public function getSettings()
44
	{
45
		return array(
46
			/*
47
			 * Specifies whether images from default theme shall be
48
			 * fetched instead of the current theme when using
49
			 * templates from the default theme.
50
			 *
51
			 * - if this is 'always', images from the default theme will be used.
52
			 * - if this is 'defaults', images from the default theme will only be used with default templates.
53
			 * - if this is 'never' or isn't set at all, images from the default theme will not be used.
54
			 *
55
			 * This doesn't apply when custom templatees are being
56 1
			 * used; nor does it apply to the dafult theme.
57
			 */
58
			'use_default_images' => 'never',
59
60
			/*
61
			 * The version this template/theme is for. This should
62
			 * be the version of the forum it was created for.
63
			 */
64
			'theme_version' => '1.0',
65
66
			/*
67
			 * Whether this theme requires the optional theme strings
68
			 * file to be loaded. (ThemeStrings.[language].php)
69
			 */
70
			'require_theme_strings' => false,
71
72
			/*
73
			 * Specify the color variants. Each variant has its own
74
			 * directory, where additional CSS files may be loaded.
75
			 *
76
			 * Example:
77
			 * - index_light.css is loaded when index.css is needed.
78
			 */
79
			'theme_variants' => array(
80
				'light',
81
				'besocial',
82
			),
83
84
			/*
85
			 * Provides avatars for use on various indexes.
86
			 *
87
			 * Possible values:
88
			 * - 0 or not set, no avatars are available
89
			 * - 1 avatar of the poster of the last message
90
			 * - 2 avatar of the poster of the first message
91
			 * - 3 both avatars
92
			 *
93
			 * Since grabbing the avatar requires some work, it is
94
			 * better to set the variable to a sensible value
95
			 * depending on the needs of the theme.
96
			 */
97
			'avatars_on_indexes' => 1,
98
99
			/*
100
			 * This is used in the main menus to create a number next
101
			 * to the title of the menu to indicate the number of
102
			 * unread messages, moderation reports, etc. You can
103
			 * style each menu level indicator as desired.
104
			 */
105
			'menu_numeric_notice' => array(
106
				// Top level menu entries
107
				0 => ' <span class="pm_indicator">%1$s</span>',
108
				// First dropdown
109
				1 => ' <span>[<strong>%1$s</strong>]</span>',
110
				// Second level dropdown
111
				2 => ' <span>[<strong>%1$s</strong>]</span>',
112
			),
113
114
			// This slightly more complex array, instead, will deal with page indexes as frequently requested by Ant :P
115
			// Oh no you don't. :D This slightly less complex array now has cleaner markup. :P
116
			// @todo - God it's still ugly though. Can't we just have links where we need them, without all those spans?
117
			// How do we get anchors only, where they will work? Spans and strong only where necessary?
118
			'page_index_template' => array(
119
				'base_link' => '<li class="linavPages"><a class="navPages" href="{base_link}" role="menuitem">%2$s</a></li>',
120
				'previous_page' => '<span class="previous_page">{prev_txt}</span>',
121
				'current_page' => '<li class="linavPages"><strong class="current_page" role="menuitem">%1$s</strong></li>',
122
				'next_page' => '<span class="next_page">{next_txt}</span>',
123
				'expand_pages' => '<li class="linavPages expand_pages" role="menuitem" {custom}> <a href="#">...</a> </li>',
124
				'all' => '<span class="linavPages all_pages">{all_txt}</span>',
125
			),
126
127
			// @todo find a better place if we are going to create a notifications template
128
			'mentions' => array(
129
				'mentioner_template' => '<a href="{mem_url}" class="mentionavatar">{avatar_img}{mem_name}</a>',
130
			)
131
		);
132
	}
133
	/**
134
	 * This is the only template included in the sources.
135
	 */
136
	public function template_rawdata()
137
	{
138
		global $context;
139
140
		echo $context['raw_data'];
141
	}
142
143
	/**
144
	 * The header template
145
	 */
146
	public function template_header()
147
	{
148
		global $context, $settings;
149
150
		doSecurityChecks();
151
152
		$this->setupThemeContext();
153
		$header = Headers::instance();
154
155
		// Print stuff to prevent caching of pages (except on attachment errors, etc.)
156
		if (empty($context['no_last_modified']))
157
		{
158
			$header
159
				->header('Expires', 'Mon, 26 Jul 1997 05:00:00 GMT')
160
				->header('Last-Modified',  gmdate('D, d M Y H:i:s') . ' GMT')
161
				->contentType('text/html', 'UTF-8');
162
		}
163
164
		// Probably temporary ($_REQUEST['xml'] should be replaced by $_REQUEST['api'])
165
		if (isset($_REQUEST['api']) && $_REQUEST['api'] === 'json')
166
		{
167
			$header->contentType('application/json', 'UTF-8');
168
		}
169
		else
170
		{
171
			$header->contentType('text/html', 'UTF-8');
172
		}
173
174
		foreach ($this->getLayers()->prepareContext() as $layer)
175
		{
176
			$this->getTemplates()->loadSubTemplate($layer . '_above', 'ignore');
177
		}
178
179
		if (isset($settings['use_default_images']) && $settings['use_default_images'] === 'defaults' && isset($settings['default_template']))
180
		{
181
			$settings['theme_url'] = $settings['default_theme_url'];
182
			$settings['images_url'] = $settings['default_images_url'];
183
			$settings['theme_dir'] = $settings['default_theme_dir'];
184
		}
185
186
		$header->sendHeaders();
187
	}
188
189
	/**
190
	 * Show the copyright.
191
	 */
192
	public function theme_copyright()
193
	{
194
		global $forum_copyright;
195
196
		// Don't display copyright for things like SSI.
197
		if (!defined('FORUM_VERSION'))
198
		{
199
			return;
200
		}
201
202
		// Put in the version...
203
		$forum_copyright = replaceBasicActionUrl(sprintf($forum_copyright, FORUM_VERSION));
204
205
		echo '
206
					', $forum_copyright;
207
	}
208
209
	/**
210
	 * The template footer
211
	 */
212
	public function template_footer()
213
	{
214
		global $context, $settings, $modSettings, $time_start;
215
216
		$db = database();
217
218
		// Show the load time?  (only makes sense for the footer.)
219
		$context['show_load_time'] = !empty($modSettings['timeLoadPageEnable']);
220
		$context['load_time'] = round(microtime(true) - $time_start, 3);
221
		$context['load_queries'] = $db->num_queries();
222
223
		if (isset($settings['use_default_images']) && $settings['use_default_images'] === 'defaults' && isset($settings['default_template']))
224
		{
225
			$settings['theme_url'] = $settings['actual_theme_url'];
226
			$settings['images_url'] = $settings['actual_images_url'];
227
			$settings['theme_dir'] = $settings['actual_theme_dir'];
228
		}
229
230
		foreach ($this->getLayers()->reverseLayers() as $layer)
231
		{
232
			$this->getTemplates()->loadSubTemplate($layer . '_below', 'ignore');
233
		}
234
	}
235
236
	/**
237
	 * Loads the required jQuery files for the system
238
	 *
239
	 * - Determines the correct script tags to add based on CDN/Local/Auto
240
	 */
241
	protected function templateJquery()
242
	{
243
		global $modSettings, $settings;
244
245
		// Using a specified version of jquery or what was shipped 3.5.1  / 1.12.1
246
		$jquery_version = (!empty($modSettings['jquery_default']) && !empty($modSettings['jquery_version'])) ? $modSettings['jquery_version'] : '3.5.1';
247
		$jqueryui_version = (!empty($modSettings['jqueryui_default']) && !empty($modSettings['jqueryui_version'])) ? $modSettings['jqueryui_version'] : '1.12.1';
248
249
		switch ($modSettings['jquery_source'])
250
		{
251
			// Only getting the files from the CDN?
252
			case 'cdn':
253
				echo '
254
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/' . $jquery_version . '/jquery.min.js" id="jquery"></script>',
255
				(!empty($modSettings['jquery_include_ui']) ? '
256
	<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/' . $jqueryui_version . '/jquery-ui.min.js" id="jqueryui"></script>' : '');
257
				break;
258
			// Just use the local file
259
			case 'local':
260
				echo '
261
	<script src="', $settings['default_theme_url'], '/scripts/jquery-' . $jquery_version . '.min.js" id="jquery"></script>',
262
				(!empty($modSettings['jquery_include_ui']) ? '
263
	<script src="' . $settings['default_theme_url'] . '/scripts/jquery-ui-' . $jqueryui_version . '.min.js" id="jqueryui"></script>' : '');
264
				break;
265
			// CDN with local fallback
266
			case 'auto':
267
				echo '
268
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/' . $jquery_version . '/jquery.min.js" id="jquery"></script>',
269
				(!empty($modSettings['jquery_include_ui']) ? '
270
	<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/' . $jqueryui_version . '/jquery-ui.min.js" id="jqueryui"></script>' : '');
271
				echo '
272
	<script>
273
		window.jQuery || document.write(\'<script src="', $settings['default_theme_url'], '/scripts/jquery-' . $jquery_version . '.min.js"><\/script>\');',
274
				(!empty($modSettings['jquery_include_ui']) ? '
275
		window.jQuery.ui || document.write(\'<script src="' . $settings['default_theme_url'] . '/scripts/jquery-ui-' . $jqueryui_version . '.min.js"><\/script>\')' : ''), '
276
	</script>';
277
				break;
278
		}
279
	}
280
281
	/**
282
	 * Loads the JS files that have been requested
283
	 *
284
	 * - Will combine / minify the files it the option is set.
285
	 * - Handles both above and below (deferred) files
286
	 *
287
	 * @param bool $do_deferred
288
	 */
289
	protected function templateJavascriptFiles($do_deferred)
290
	{
291
		global $modSettings, $settings;
292
293
		// Combine and minify javascript source files to save bandwidth and requests
294
		if (!empty($modSettings['minify_css_js']))
295
		{
296
			$combiner = new SiteCombiner($settings['default_theme_cache_dir'], $settings['default_theme_cache_url']);
297
			$combine_name = $combiner->site_js_combine($this->js_files, $do_deferred);
298
299
			call_integration_hook('post_javascript_combine', array(&$combine_name, $combiner));
300
301
			if (!empty($combine_name))
302
			{
303
				echo '
304
	<script src="', $combine_name, '" id="jscombined', $do_deferred ? 'bottom' : 'top', '"></script>';
305
			}
306
			// While we have Javascript files to place in the template
307
			foreach ($combiner->getSpares() as $id => $js_file)
308
			{
309
				if ((!$do_deferred && empty($js_file['options']['defer'])) || ($do_deferred && !empty($js_file['options']['defer'])))
310
				{
311
					echo '
312
	<script src="', $js_file['filename'], '" id="', $id, '"', !empty($js_file['options']['async']) ? ' async="async"' : '', '></script>';
313
				}
314
			}
315
		}
316
		// Just give them the full load then
317
		else
318
		{
319
			// While we have Javascript files to place in the template
320
			foreach ($this->js_files as $id => $js_file)
321
			{
322
				if ((!$do_deferred && empty($js_file['options']['defer'])) || ($do_deferred && !empty($js_file['options']['defer'])))
323
				{
324
					echo '
325
	<script src="', $js_file['filename'], '" id="', $id, '"', !empty($js_file['options']['async']) ? ' async="async"' : '', '></script>';
326
				}
327
			}
328
		}
329
	}
330
331
	/**
332
	 * Deletes the hives (aggregated CSS and JS files) previously created.
333
	 *
334
	 * @param string $type           = 'all' Filters the types of hives (valid values:
335
	 *                               * 'all'
336
	 *                               * 'css'
337
	 *                               * 'js'
338
	 *
339
	 * @return bool
340
	 */
341
	public function cleanHives($type = 'all')
342
	{
343
		global $settings;
344
345
		$combiner = new SiteCombiner($settings['default_theme_cache_dir'], $settings['default_theme_cache_url']);
346
		$result = true;
347
348
		if ($type === 'all' || $type === 'css')
349
		{
350
			$result &= $combiner->removeCssHives();
351
		}
352
353
		if ($type === 'all' || $type === 'js')
354
		{
355
			$result &= $combiner->removeJsHives();
356
		}
357
358
		return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result also could return the type integer which is incompatible with the documented return type boolean.
Loading history...
359
	}
360
361
	/**
362
	 * Output the Javascript files
363
	 *
364
	 * What it does:
365
	 *
366
	 * - Tabbing in this function is to make the HTML source look proper
367
	 * - Outputs jQuery/jQueryUI from the proper source (local/CDN)
368
	 * - If deferred is set function will output all JS (source & inline) set to load at page end
369
	 * - If the admin option to combine files is set, will use Combiner.class
370
	 *
371
	 * @param bool $do_deferred = false
372
	 */
373
	public function template_javascript($do_deferred = false)
374
	{
375
		global $modSettings;
376
377
		// First up, load jQuery and jQuery UI
378
		if (isset($modSettings['jquery_source']) && !$do_deferred)
379
		{
380
			$this->templateJquery();
381
		}
382
383
		// Use this hook to work with Javascript files and vars pre output
384
		call_integration_hook('pre_javascript_output', array($do_deferred));
385
386
		// Load in the JS files
387
		if (!empty($this->js_files))
388
		{
389
			$this->templateJavascriptFiles($do_deferred);
390
		}
391
392
		// Build the declared Javascript variables script
393
		$js_vars = array();
394
		if (!empty($this->js_vars) && !$do_deferred)
395
		{
396
			foreach ($this->js_vars as $var => $value)
397
				$js_vars[] = $var . ' = ' . $value;
398
399
			// Newlines and tabs are here to make it look nice in the page source view, stripped if minimized though
400
			$this->js_inline['standard'][] = 'var ' . implode(",\n\t\t\t", $js_vars) . ';';
401
		}
402
403
		// Inline JavaScript - Actually useful some times!
404
		if (!empty($this->js_inline))
405
		{
406
			// Deferred output waits until we are deferring !
407
			if (!empty($this->js_inline['defer']) && $do_deferred)
408
			{
409
				// Combine them all in to one output
410
				$this->js_inline['defer'] = array_map('trim', $this->js_inline['defer']);
411
				$inline_defered_code = implode("\n\t\t", $this->js_inline['defer']);
412
413
				// Output the deferred script
414
				echo '
415
	<script>
416
		', $inline_defered_code, '
417
	</script>';
418
			}
419
420
			// Standard output, and our javascript vars, get output when we are not on a defered call
421
			if (!empty($this->js_inline['standard']) && !$do_deferred)
422
			{
423
				$this->js_inline['standard'] = array_map('trim', $this->js_inline['standard']);
424
425
				// And output the js vars and standard scripts to the page
426
				echo '
427
	<script>
428
		', implode("\n\t\t", $this->js_inline['standard']), '
429
	</script>';
430
			}
431
		}
432
	}
433
434
	/**
435
	 * Output the CSS files
436
	 *
437
	 * What it does:
438
	 *  - If the admin option to combine files is set, will use Combiner.class
439
	 */
440
	public function template_css()
441
	{
442
		global $modSettings, $settings;
443
444
		// Use this hook to work with CSS files pre output
445
		call_integration_hook('pre_css_output');
446
447
		// Combine and minify the CSS files to save bandwidth and requests?
448
		if (!empty($this->css_files))
449
		{
450
			if (!empty($modSettings['minify_css_js']))
451
			{
452
				$combiner = new SiteCombiner($settings['default_theme_cache_dir'], $settings['default_theme_cache_url']);
453
				$combine_name = $combiner->site_css_combine($this->css_files);
454
455
				call_integration_hook('post_css_combine', array(&$combine_name, $combiner));
456
457
				if (!empty($combine_name))
458
				{
459
					echo '
460
	<link rel="stylesheet" href="', $combine_name, '" id="csscombined" />';
461
				}
462
463
				foreach ($combiner->getSpares() as $id => $file)
464
					echo '
465
	<link rel="stylesheet" href="', $file['filename'], '" id="', $id, '" />';
466
			}
467
			else
468
			{
469
				foreach ($this->css_files as $id => $file)
470
					echo '
471
	<link rel="stylesheet" href="', $file['filename'], '" id="', $id, '" />';
472
			}
473
		}
474
	}
475
476
	/**
477
	 * Output the inline-CSS in a style tag
478
	 */
479
	public function template_inlinecss()
480
	{
481
		$style_tag = '';
482
483
		// Combine and minify the CSS files to save bandwidth and requests?
484
		if (!empty($this->css_rules))
485
		{
486
			if (!empty($this->css_rules['all']))
487
			{
488
				$style_tag .= '
489
	' . $this->css_rules['all'];
490
			}
491
			if (!empty($this->css_rules['media']))
492
			{
493
				foreach ($this->css_rules['media'] as $key => $val)
494
				{
495
					$style_tag .= '
496
	@media ' . $key . '{
497
		' . $val . '
498
	}';
499
				}
500
			}
501
		}
502
503
		if (!empty($style_tag))
504
		{
505
			echo '
506
	<style>' . $style_tag . '
507
	</style>';
508
		}
509
	}
510
511
	/**
512
	 * Calls on template_show_error from index.template.php to show warnings
513
	 * and security errors for admins
514
	 */
515
	public function template_admin_warning_above()
516
	{
517
		global $context, $txt;
518
519
		if (!empty($context['security_controls_files']))
520
		{
521
			$context['security_controls_files']['type'] = 'serious';
522
			template_show_error('security_controls_files');
523
		}
524
525
		if (!empty($context['security_controls_query']))
526
		{
527
			$context['security_controls_query']['type'] = 'serious';
528
			template_show_error('security_controls_query');
529
		}
530
531
		if (!empty($context['security_controls_ban']))
532
		{
533
			$context['security_controls_ban']['type'] = 'serious';
534
			template_show_error('security_controls_ban');
535
		}
536
537
		if (!empty($context['new_version_updates']))
538
		{
539
			template_show_error('new_version_updates');
540
		}
541
542
		if (!empty($context['accepted_agreement']))
543
		{
544
			template_show_error('accepted_agreement');
545
		}
546
547
		// Any special notices to remind the admin about?
548
		if (!empty($context['warning_controls']))
549
		{
550
			$context['warning_controls']['errors'] = $context['warning_controls'];
551
			$context['warning_controls']['title'] = $txt['admin_warning_title'];
552
			$context['warning_controls']['type'] = 'warning';
553
			template_show_error('warning_controls');
554
		}
555
	}
556
557
	/**
558
	 * If the option to pretty output code is on, this loads the JS and CSS
559
	 */
560
	public function addCodePrettify()
561
	{
562
		global $modSettings;
563
564
		if (!empty($modSettings['enableCodePrettify']))
565
		{
566
			loadCSSFile('prettify.css');
567
			loadJavascriptFile('prettify.min.js', array('defer' => true));
568
569
			$this->addInlineJavascript('
570
			$(function() {
571
				prettyPrint();
572
			});', true);
573
		}
574
	}
575
576
	/**
577
	 * If video embedding is enabled, this loads the needed JS and vars
578
	 */
579
	public function autoEmbedVideo()
580
	{
581
		global $txt, $modSettings;
582
583
		if (!empty($modSettings['enableVideoEmbeding']))
584
		{
585
			$this->addInlineJavascript('
586 1
			var oEmbedtext = ({
587
				embed_limit : ' . (!empty($modSettings['video_embed_limit']) ? $modSettings['video_embed_limit'] : 25) . ',
588 1
				preview_image : ' . JavaScriptEscape($txt['preview_image']) . ',
589
				ctp_video : ' . JavaScriptEscape($txt['ctp_video']) . ',
590 1
				hide_video : ' . JavaScriptEscape($txt['hide_video']) . ',
591
				youtube : ' . JavaScriptEscape($txt['youtube']) . ',
592
				vimeo : ' . JavaScriptEscape($txt['vimeo']) . ',
593
				dailymotion : ' . JavaScriptEscape($txt['dailymotion']) . '
594
			});', true);
595
596
			loadJavascriptFile('elk_jquery_embed.js', array('defer' => true));
597
		}
598
	}
599
600 1
	/**
601
	 * Ensures we kick the mail queue from time to time so that it gets
602
	 * checked as often as possible.
603
	 */
604
	public function doScheduledSendMail()
605 1
	{
606
		global $modSettings;
607 1
608
		if (isBrowser('possibly_robot'))
609 1
		{
610
			// @todo Maybe move this somewhere better?!
611
			$controller = new ScheduledTasks(new EventManager());
612
613
			// What to do, what to do?!
614
			if (empty($modSettings['next_task_time']) || $modSettings['next_task_time'] < time())
615
			{
616
				$controller->action_autotask();
617
			}
618
			else
619
			{
620
				$controller->action_reducemailqueue();
621
			}
622
		}
623
		else
624 1
		{
625
			$type = empty($modSettings['next_task_time']) || $modSettings['next_task_time'] < time() ? 'task' : 'mailq';
626
			$ts = $type === 'mailq' ? $modSettings['mail_next_send'] : $modSettings['next_task_time'];
627
628
			$this->addInlineJavascript('
629
		function elkAutoTask()
630 1
		{
631
			var tempImage = new Image();
632 1
			tempImage.src = elk_scripturl + "?scheduled=' . $type . ';ts=' . $ts . '";
633
		}
634 1
		window.setTimeout("elkAutoTask();", 1);', true);
635
		}
636
	}
637 1
638
	/**
639
	 * Relative times require a few variables be set in the JS
640 1
	 */
641
	public function relativeTimes()
642 1
	{
643
		global $modSettings, $context, $txt;
644
645
		// Relative times?
646 1
		if (!empty($modSettings['todayMod']) && $modSettings['todayMod'] > 2)
647
		{
648
			$this->addInlineJavascript('
649
			var oRttime = ({
650
				referenceTime : ' . forum_time() * 1000 . ',
651
				now : ' . JavaScriptEscape($txt['rt_now']) . ',
652
				minute : ' . JavaScriptEscape($txt['rt_minute']) . ',
653
				minutes : ' . JavaScriptEscape($txt['rt_minutes']) . ',
654
				hour : ' . JavaScriptEscape($txt['rt_hour']) . ',
655
				hours : ' . JavaScriptEscape($txt['rt_hours']) . ',
656
				day : ' . JavaScriptEscape($txt['rt_day']) . ',
657
				days : ' . JavaScriptEscape($txt['rt_days']) . ',
658
				week : ' . JavaScriptEscape($txt['rt_week']) . ',
659
				weeks : ' . JavaScriptEscape($txt['rt_weeks']) . ',
660
				month : ' . JavaScriptEscape($txt['rt_month']) . ',
661
				months : ' . JavaScriptEscape($txt['rt_months']) . ',
662 1
				year : ' . JavaScriptEscape($txt['rt_year']) . ',
663
				years : ' . JavaScriptEscape($txt['rt_years']) . ',
664
			});
665
			updateRelativeTime();', true);
666
667 1
			$context['using_relative_time'] = true;
668
		}
669 1
	}
670
671
	/**
672 1
	 * Sets up the basic theme context stuff.
673
	 *
674
	 * @param bool $forceload = false
675
	 */
676
	public function setupThemeContext($forceload = false)
677
	{
678
		global $modSettings, $scripturl, $context, $settings, $options, $txt, $boardurl;
679
680
		static $loaded = false;
681
682
		// Under SSI this function can be called more then once.  That can cause some problems.
683
		// So only run the function once unless we are forced to run it again.
684
		if ($loaded && !$forceload)
685
		{
686
			return;
687
		}
688
689
		$loaded = true;
690
691
		$context['current_time'] = standardTime(time(), false);
692
		$context['current_action'] = isset($_GET['action']) ? $_GET['action'] : '';
693
		$context['show_quick_login'] = !empty($modSettings['enableVBStyleLogin']) && $this->user->is_guest;
694
		$context['robot_no_index'] = in_array($context['current_action'], $this->no_index_actions);
695 1
696
		$bbc_parser = ParserWrapper::instance();
697
698
		// Get some news...
699
		$context['news_lines'] = array_filter(explode("\n", str_replace("\r", '', trim(addslashes($modSettings['news'])))));
700
		for ($i = 0, $n = count($context['news_lines']); $i < $n; $i++)
701
		{
702
			if (trim($context['news_lines'][$i]) === '')
703
			{
704
				continue;
705
			}
706
707
			// Clean it up for presentation ;).
708
			$context['news_lines'][$i] = $bbc_parser->parseNews(stripslashes(trim($context['news_lines'][$i])));
709
		}
710
711
		// If we have some, setup for display
712
		if (!empty($context['news_lines']))
713
		{
714
			$context['random_news_line'] = $context['news_lines'][mt_rand(0, count($context['news_lines']) - 1)];
715
			$context['upper_content_callbacks'][] = 'news_fader';
716
		}
717
718
		if ($this->user->is_guest === false)
719
		{
720
			$context['user']['messages'] = $this->user->messages;
721
			$context['user']['unread_messages'] = $this->user->unread_messages;
722
			$context['user']['mentions'] = $this->user->mentions;
723
724
			// Personal message popup...
725
			if ($this->user->unread_messages > (isset($_SESSION['unread_messages']) ? $_SESSION['unread_messages'] : 0))
726
			{
727
				$context['user']['popup_messages'] = true;
728
			}
729
			else
730
			{
731
				$context['user']['popup_messages'] = false;
732
			}
733
734
			$_SESSION['unread_messages'] = $this->user->unread_messages;
735
736
			$context['user']['avatar'] = array(
737
				'href' => !empty($this->user->avatar['href']) ? $this->user->avatar['href'] : '',
738
				'image' => !empty($this->user->avatar['image']) ? $this->user->avatar['image'] : '',
739
			);
740
741
			// Figure out how long they've been logged in.
742
			$context['user']['total_time_logged_in'] = array(
743
				'days' => floor($this->user->total_time_logged_in / 86400),
744
				'hours' => floor(($this->user->total_time_logged_in % 86400) / 3600),
745
				'minutes' => floor(($this->user->total_time_logged_in % 3600) / 60)
746
			);
747
		}
748
		else
749
		{
750
			$context['user']['messages'] = 0;
751
			$context['user']['unread_messages'] = 0;
752
			$context['user']['mentions'] = 0;
753
			$context['user']['avatar'] = array();
754
			$context['user']['total_time_logged_in'] = array('days' => 0, 'hours' => 0, 'minutes' => 0);
755
			$context['user']['popup_messages'] = false;
756
757
			if (!empty($modSettings['registration_method']) && $modSettings['registration_method'] == 1)
758
			{
759
				$txt['welcome_guest'] .= $txt['welcome_guest_activate'];
760
			}
761
762
			$txt['welcome_guest'] = replaceBasicActionUrl($txt['welcome_guest']);
763
764
			// If we've upgraded recently, go easy on the passwords.
765
			if (!empty($modSettings['enable_password_conversion']))
766
			{
767
				$context['disable_login_hashing'] = true;
768
			}
769
		}
770
771
		// Setup the main menu items.
772
		$this->setupMenuContext();
773
774
		if (empty($settings['theme_version']))
775
		{
776
			$context['show_vBlogin'] = $context['show_quick_login'];
777
		}
778
779
		// This is here because old index templates might still use it.
780
		$context['show_news'] = !empty($settings['enable_news']);
781
782
		$context['additional_dropdown_search'] = prepareSearchEngines();
783
784
		// This is done to allow theme authors to customize it as they want.
785
		$context['show_pm_popup'] = $context['user']['popup_messages'] && !empty($options['popup_messages']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] !== 'pm');
786
787
		// Add the PM popup here instead. Theme authors can still override it simply by editing/removing the 'fPmPopup' in the array.
788
		if ($context['show_pm_popup'])
789
		{
790
			$this->addInlineJavascript('
791
			$(function() {
792
				new smc_Popup({
793
					heading: ' . JavaScriptEscape($txt['show_personal_messages_heading']) . ',
794
					content: ' . JavaScriptEscape(sprintf($txt['show_personal_messages'], $context['user']['unread_messages'], $scripturl . '?action=pm')) . ',
795
					icon: \'i-envelope\'
796
				});
797
			});', true);
798
		}
799
800
		// This looks weird, but it's because BoardIndex.controller.php references the variable.
801
		$href = getUrl('profile', ['action' => 'profile', 'u' => $modSettings['latestMember'], 'name' => $modSettings['latestRealName']]);
802
		$context['common_stats']['latest_member'] = array(
803
			'id' => $modSettings['latestMember'],
804
			'name' => $modSettings['latestRealName'],
805
			'href' => $href,
806
			'link' => '<a href="' . $href . '">' . $modSettings['latestRealName'] . '</a>',
807
		);
808
809
		$context['common_stats'] = array(
810
			'total_posts' => comma_format($modSettings['totalMessages']),
811
			'total_topics' => comma_format($modSettings['totalTopics']),
812
			'total_members' => comma_format($modSettings['totalMembers']),
813
			'latest_member' => $context['common_stats']['latest_member'],
814
		);
815
816
		$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']);
817
818
		if (empty($settings['theme_version']))
819
		{
820
			$this->addJavascriptVar(array('elk_scripturl' => $scripturl), true);
821
		}
822
		$this->addJavascriptVar(array('elk_forum_action' =>  getUrlQuery('action', $modSettings['default_forum_action'])), true);
823
824
		if (!isset($context['page_title']))
825
		{
826
			$context['page_title'] = '';
827
		}
828
829
		// Set some specific vars.
830
		$context['page_title_html_safe'] = Util::htmlspecialchars(un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
831
832
		$context['favicon'] = $boardurl . '/mobile.png';
833
834
		$this->loadSupportCSS();
835
836
		// Since it's nice to have avatars all of the same size, and in some cases the size detection may fail,
837
		// let's add the css in any case
838
		if (!isset($context['html_headers']))
839
			$context['html_headers'] = '';
840
841
		if (!empty($modSettings['avatar_max_width']) || !empty($modSettings['avatar_max_height']))
842
		{
843
			$this->addCSSRules('
844
		.avatarresize {' . (!empty($modSettings['avatar_max_width']) ? '
845
			max-width:' . $modSettings['avatar_max_width'] . 'px;' : '') . (!empty($modSettings['avatar_max_height']) ? '
846
			max-height:' . $modSettings['avatar_max_height'] . 'px;' : '') . '
847
		}');
848
		}
849
850
		// Save some database hits, if a width for multiple wrappers is set in admin.
851
		if (!empty($settings['forum_width']))
852
		{
853
			$this->addCSSRules('
854
		.wrapper {width: ' . $settings['forum_width'] . ';}');
855
		}
856
	}
857
858
	/**
859
	 * Adds required support CSS files.
860
	 */
861
	public function loadSupportCSS()
862
	{
863
		global $settings;
864
865
		// Load the SVG support file with fallback to default theme
866
		loadCSSFile('icons_svg.css');
867
868
		// Load a base theme custom CSS file?
869
		if (file_exists($settings['theme_dir'] . '/css/custom.css'))
870
		{
871
			loadCSSFile('custom.css');
872
		}
873
	}
874
875
	/**
876
	 * Sets up all of the top menu buttons
877
	 *
878
	 * What it does:
879
	 *
880
	 * - Defines every master item in the menu, as well as any sub-items
881
	 * - Ensures the chosen action is set so the menu is highlighted
882
	 * - Saves them in the cache if it is available and on
883
	 * - Places the results in $context
884
	 */
885
	public function setupMenuContext()
886
	{
887
		global $context, $modSettings, $settings;
888
889
		// Set up the menu privileges.
890
		$context['allow_search'] = !empty($modSettings['allow_guestAccess']) ? allowedTo('search_posts') : ($this->user->is_guest === false && allowedTo('search_posts'));
891
		$context['allow_admin'] = allowedTo(array('admin_forum', 'manage_boards', 'manage_permissions', 'moderate_forum', 'manage_membergroups', 'manage_bans', 'send_mail', 'edit_news', 'manage_attachments', 'manage_smileys'));
892
		$context['allow_edit_profile'] = $this->user->is_guest === false && allowedTo(array('profile_view_own', 'profile_view_any', 'profile_identity_own', 'profile_identity_any', 'profile_extra_own', 'profile_extra_any', 'profile_remove_own', 'profile_remove_any', 'moderate_forum', 'manage_membergroups', 'profile_title_own', 'profile_title_any'));
893
		$context['allow_memberlist'] = allowedTo('view_mlist');
894
		$context['allow_calendar'] = allowedTo('calendar_view') && !empty($modSettings['cal_enabled']);
895
		$context['allow_moderation_center'] = $context['user']['can_mod'];
896
		$context['allow_pm'] = allowedTo('pm_read');
897
		$cache = Cache::instance();
898
899
		call_integration_hook('integrate_setup_allow');
900
901
		if ($context['allow_search'])
902
		{
903
			$context['theme_header_callbacks'] = elk_array_insert($context['theme_header_callbacks'], 'login_bar', array('search_bar'), 'after');
904
		}
905
906
		$cacheTime = $modSettings['lastActive'] * 60;
907
908
		// Update the Moderation menu items with action item totals
909
		if ($context['allow_moderation_center'])
910
		{
911
			// Get the numbers for the menu ...
912
			require_once(SUBSDIR . '/Moderation.subs.php');
913
			$menu_count = loadModeratorMenuCounts();
914
		}
915
916
		$menu_count['unread_messages'] = $context['user']['unread_messages'];
917
		$menu_count['mentions'] = $context['user']['mentions'];
918
919
		if (!empty($this->user->avatar['href']))
920
		{
921
			$this->addCSSRules('
922
	.i-account:before {
923
		content: "";
924
		background-image: url("' . $this->user->avatar['href'] . '");
925
	}');
926
		}
927
928
		// All the buttons we can possible want and then some, try pulling the final list of buttons from cache first.
929
		if (($menu_buttons = $cache->get('menu_buttons-' . implode('_', $this->user->groups) . '-' . $this->user->language, $cacheTime)) === null || time() - $cacheTime <= $modSettings['settings_updated'])
0 ignored issues
show
Bug introduced by
It seems like $this->user->groups can also be of type null; however, parameter $pieces of implode() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

929
		if (($menu_buttons = $cache->get('menu_buttons-' . implode('_', /** @scrutinizer ignore-type */ $this->user->groups) . '-' . $this->user->language, $cacheTime)) === null || time() - $cacheTime <= $modSettings['settings_updated'])
Loading history...
930
		{
931
			// Start things up: this is what we know by default
932
			require_once(SUBSDIR . '/Menu.subs.php');
933
			$buttons = loadDefaultMenuButtons();
934
935
			// Allow editing menu buttons easily.
936
			call_integration_hook('integrate_menu_buttons', array(&$buttons, &$menu_count));
937
938
			// Now we put the buttons in the context so the theme can use them.
939
			$menu_buttons = array();
940
			foreach ($buttons as $act => $button)
941
			{
942
				if (!empty($button['show']))
943
				{
944
					$button['active_button'] = false;
945
946
					// This button needs some action.
947
					if (isset($button['action_hook']))
948
					{
949
						$needs_action_hook = true;
950
					}
951
952
					if (isset($button['counter']) && !empty($menu_count[$button['counter']]))
953
					{
954
						$button['alttitle'] = $button['title'] . ' [' . $menu_count[$button['counter']] . ']';
955
						if (!empty($settings['menu_numeric_notice'][0]))
956
						{
957
							$button['title'] .= sprintf($settings['menu_numeric_notice'][0], $menu_count[$button['counter']]);
958
							$button['indicator'] = true;
959
						}
960
					}
961
962
					// Go through the sub buttons if there are any.
963
					if (isset($button['sub_buttons']))
964
					{
965
						foreach ($button['sub_buttons'] as $key => $subbutton)
966
						{
967
							if (empty($subbutton['show']))
968
							{
969
								unset($button['sub_buttons'][$key]);
970
							}
971
							elseif (isset($subbutton['counter']) && !empty($menu_count[$subbutton['counter']]))
972
							{
973
								$button['sub_buttons'][$key]['alttitle'] = $subbutton['title'] . ' [' . $menu_count[$subbutton['counter']] . ']';
974
								if (!empty($settings['menu_numeric_notice'][1]))
975
								{
976
									$button['sub_buttons'][$key]['title'] .= sprintf($settings['menu_numeric_notice'][1], $menu_count[$subbutton['counter']]);
977
								}
978
979
								// 2nd level sub buttons next...
980
								if (isset($subbutton['sub_buttons']))
981
								{
982
									foreach ($subbutton['sub_buttons'] as $key2 => $subbutton2)
983
									{
984
										$button['sub_buttons'][$key]['sub_buttons'][$key2] = $subbutton2;
985
										if (empty($subbutton2['show']))
986
										{
987
											unset($button['sub_buttons'][$key]['sub_buttons'][$key2]);
988
										}
989
										elseif (isset($subbutton2['counter']) && !empty($menu_count[$subbutton2['counter']]))
990
										{
991
											$button['sub_buttons'][$key]['sub_buttons'][$key2]['alttitle'] = $subbutton2['title'] . ' [' . $menu_count[$subbutton2['counter']] . ']';
992
											if (!empty($settings['menu_numeric_notice'][2]))
993
											{
994
												$button['sub_buttons'][$key]['sub_buttons'][$key2]['title'] .= sprintf($settings['menu_numeric_notice'][2], $menu_count[$subbutton2['counter']]);
995
											}
996
											unset($menu_count[$subbutton2['counter']]);
997
										}
998
									}
999
								}
1000
							}
1001
						}
1002
					}
1003
1004
					$menu_buttons[$act] = $button;
1005
				}
1006
			}
1007
1008
1009
			if ($cache->levelHigherThan(1))
1010
			{
1011
				$cache->put('menu_buttons-' . implode('_', $this->user->groups) . '-' . $this->user->language, $menu_buttons, $cacheTime);
1012
			}
1013
		}
1014
1015
		if (!empty($menu_buttons['profile']['sub_buttons']['logout']))
1016
		{
1017
			$menu_buttons['profile']['sub_buttons']['logout']['href'] .= ';' . $context['session_var'] . '=' . $context['session_id'];
1018
		}
1019
1020
		$context['menu_buttons'] = $menu_buttons;
1021
1022
		// Figure out which action we are doing so we can set the active tab.
1023
		// Default to home.
1024
		$current_action = 'home';
1025
1026
		if (isset($context['menu_buttons'][$context['current_action']]))
1027
		{
1028
			$current_action = $context['current_action'];
1029
		}
1030
		elseif ($context['current_action'] === 'profile')
1031
		{
1032
			$current_action = 'pm';
1033
		}
1034
		elseif ($context['current_action'] === 'theme')
1035
		{
1036
			$current_action = isset($_REQUEST['sa']) && $_REQUEST['sa'] === 'pick' ? 'profile' : 'admin';
1037
		}
1038
		elseif ($context['current_action'] === 'login2' || ($this->user->is_guest && $context['current_action'] === 'reminder'))
1039
		{
1040
			$current_action = 'login';
1041
		}
1042
		elseif ($context['current_action'] === 'groups' && $context['allow_moderation_center'])
1043
		{
1044
			$current_action = 'moderate';
1045
		}
1046
		elseif ($context['current_action'] === 'moderate' && $context['allow_admin'])
1047
		{
1048
			$current_action = 'admin';
1049
		}
1050
1051
		// Not all actions are simple.
1052
		if (!empty($needs_action_hook))
1053
		{
1054
			call_integration_hook('integrate_current_action', array(&$current_action));
1055
		}
1056
1057
		if (isset($context['menu_buttons'][$current_action]))
1058
		{
1059
			$context['menu_buttons'][$current_action]['active_button'] = true;
1060
		}
1061
	}
1062
1063
	/**
1064
	 * Load the base JS that gives Elkarte a nice rack
1065
	 */
1066
	public function loadThemeJavascript()
1067
	{
1068
		global $settings, $context, $modSettings, $scripturl, $txt, $options;
1069
1070
		// Queue our Javascript
1071
		loadJavascriptFile(array('elk_jquery_plugins.js', 'script.js', 'script_elk.js', 'theme.js'));
1072
1073
		// Default JS variables for use in every theme
1074
		$this->addJavascriptVar(array(
1075
				'elk_theme_url' => JavaScriptEscape($settings['theme_url']),
1076
				'elk_default_theme_url' => JavaScriptEscape($settings['default_theme_url']),
1077
				'elk_images_url' => JavaScriptEscape($settings['images_url']),
1078
				'elk_smiley_url' => JavaScriptEscape($modSettings['smileys_url']),
1079
				'elk_scripturl' => '\'' . $scripturl . '\'',
1080
				'elk_iso_case_folding' => detectServer()->is('iso_case_folding') ? 'true' : 'false',
1081
				'elk_charset' => '"UTF-8"',
1082
				'elk_session_id' => JavaScriptEscape($context['session_id']),
1083
				'elk_session_var' => JavaScriptEscape($context['session_var']),
1084
				'elk_member_id' => $context['user']['id'],
1085
				'ajax_notification_text' => JavaScriptEscape($txt['ajax_in_progress']),
1086
				'ajax_notification_cancel_text' => JavaScriptEscape($txt['modify_cancel']),
1087
				'help_popup_heading_text' => JavaScriptEscape($txt['help_popup']),
1088
				'use_click_menu' => !empty($options['use_click_menu']) ? 'true' : 'false',
1089
				'todayMod' => !empty($modSettings['todayMod']) ? (int) $modSettings['todayMod'] : 0)
1090 1
		);
1091
1092 1
		// Auto video embedding enabled, then load the needed JS
1093
		$this->autoEmbedVideo();
1094
1095 1
		// Prettify code tags? Load the needed JS and CSS.
1096
		$this->addCodePrettify();
1097
1098 1
		// Relative times for posts?
1099 1
		$this->relativeTimes();
1100 1
1101 1
		// If we think we have mail to send, let's offer up some possibilities... robots get pain (Now with scheduled task support!)
1102 1
		if ((!empty($modSettings['mail_next_send']) && $modSettings['mail_next_send'] < time() && empty($modSettings['mail_queue_use_cron'])) || empty($modSettings['next_task_time']) || $modSettings['next_task_time'] < time())
1103 1
		{
1104 1
			$this->doScheduledSendMail();
1105 1
		}
1106 1
	}
1107 1
1108 1
	/**
1109 1
	 * Makes the default layers and languages available
1110 1
	 *
1111 1
	 * - Loads index and addon language files as needed
1112 1
	 * - Loads xml, index or no templates as needed
1113 1
	 * - Loads templates as defined by $settings['theme_templates']
1114
	 */
1115
	public function loadDefaultLayers()
1116
	{
1117 1
		global $settings;
1118
1119
		$simpleActions = array(
1120 1
			'quickhelp',
1121
			'printpage',
1122
			'quotefast',
1123 1
			'spellcheck',
1124
		);
1125
1126 1
		call_integration_hook('integrate_simple_actions', array(&$simpleActions));
1127
1128 1
		// Output is fully XML
1129
		if (isset($_REQUEST['xml']))
1130 1
		{
1131
			\ElkArte\Themes\ThemeLoader::loadLanguageFile('index+Addons');
1132
			$this->getTemplates()->load('Xml');
1133
			$this->getLayers()->removeAll();
1134
		}
1135
		// These actions don't require the index template at all.
1136
		elseif (!empty($_REQUEST['action']) && in_array($_REQUEST['action'], $simpleActions))
1137
		{
1138
			\ElkArte\Themes\ThemeLoader::loadLanguageFile('index+Addons');
1139 1
			$this->getLayers()->removeAll();
1140
		}
1141 1
		else
1142
		{
1143
			// Custom templates to load, or just default?
1144 1
			$templates = isset($settings['theme_templates']) ? explode(',', $settings['theme_templates']) : ['index'];
1145
1146
			// Load each template...
1147
			foreach ($templates as $template)
1148
				$this->getTemplates()->load($template);
1149
1150 1
			// ...and attempt to load their associated language files.
1151
			ThemeLoader::loadLanguageFiles(array_merge($templates, ['Addons']), '', false);
1152
1153 1
			// Custom template layers?
1154
			$layers = isset($settings['theme_layers']) ? explode(',', $settings['theme_layers']) : ['html', 'body'];
1155
1156
			$template_layers = $this->getLayers();
1157
			$template_layers->setErrorSafeLayers($layers);
1158
			foreach ($layers as $layer)
1159
			{
1160 1
				$template_layers->addBegin($layer);
1161
			}
1162
		}
1163
	}
1164
1165
	/**
1166
	 * If a variant CSS is needed, this loads it
1167
	 */
1168 1
	public function loadThemeVariant()
1169
	{
1170
		global $context, $settings, $options;
1171 1
1172 1
		// Overriding - for previews and that ilk.
1173
		if (!empty($_REQUEST['variant']))
1174
		{
1175 1
			$_SESSION['id_variant'] = $_REQUEST['variant'];
1176
		}
1177
1178 1
		// User selection?
1179
		if (empty($settings['disable_user_variant']) || allowedTo('admin_forum'))
1180 1
		{
1181 1
			$context['theme_variant'] = !empty($_SESSION['id_variant']) ? $_SESSION['id_variant'] : (!empty($options['theme_variant']) ? $options['theme_variant'] : '');
1182 1
		}
1183
1184 1
		// If not a user variant, select the default.
1185
		if ($context['theme_variant'] === '' || !in_array($context['theme_variant'], $settings['theme_variants']))
1186
		{
1187 1
			$context['theme_variant'] = !empty($settings['default_variant']) && in_array($settings['default_variant'], $settings['theme_variants']) ? $settings['default_variant'] : $settings['theme_variants'][0];
1188
		}
1189
1190
		// Do this to keep things easier in the templates.
1191
		$context['theme_variant'] = '_' . $context['theme_variant'];
1192 1
		$context['theme_variant_url'] = $context['theme_variant'] . '/';
1193
1194 1
		// The most efficient way of writing multi themes is to use a master index.css plus variant.css files.
1195
		if (!empty($context['theme_variant']))
1196
		{
1197 1
			loadCSSFile($context['theme_variant'] . '/index' . $context['theme_variant'] . '.css');
1198
1199
			// Variant icon definitions?
1200
			if (file_exists($settings['theme_dir'] . '/css/' . $context['theme_variant'] . '/icons_svg' . $context['theme_variant'] . '.css'))
1201
			{
1202
				loadCSSFile($context['theme_variant'] . '/icons_svg' . $context['theme_variant'] . '.css');
1203 1
			}
1204
1205 1
			// Load a theme variant custom CSS
1206
			if (!empty($context['theme_variant']) && file_exists($settings['theme_dir'] . '/css/' . $context['theme_variant'] . '/custom' . $context['theme_variant'] . '.css'))
1207
			{
1208
				loadCSSFile($context['theme_variant'] . '/custom' . $context['theme_variant'] . '.css');
1209 1
			}
1210
		}
1211 1
	}
1212
}
1213