ManageSmileys::action_smileySettings_display()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 35
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 15
nc 2
nop 0
dl 0
loc 35
ccs 0
cts 14
cp 0
crap 6
rs 9.7666
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file takes care of all administration of smileys.
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 Beta 1
14
 *
15
 */
16
17
namespace ElkArte\AdminController;
18
19
use BBC\ParserWrapper;
20
use ElkArte\AbstractController;
21
use ElkArte\Action;
22
use ElkArte\Cache\Cache;
23
use ElkArte\Errors\Errors;
0 ignored issues
show
Bug introduced by
The type ElkArte\Errors\Errors was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
24
use ElkArte\Exceptions\Exception;
25
use ElkArte\Helper\FileFunctions;
26
use ElkArte\Helper\Util;
27
use ElkArte\Languages\Txt;
28
use ElkArte\Packages\PackageChmod;
29
use ElkArte\Packages\PackageParser;
30
use ElkArte\SettingsForm\SettingsForm;
31
32
/**
33
 * This class is in charge with administration of smileys and message icons.
34
 * It handles actions from the Smileys pages in admin panel.
35
 */
36
class ManageSmileys extends AbstractController
37
{
38
	/** @var array Contextual information about smiley sets. */
39
	private array $_smiley_context = [];
40
41
	/** @var string[] allowed extensions for smiles */
42
	private array $_smiley_types = ['jpg', 'gif', 'jpeg', 'png', 'webp', 'svg'];
43
44
	/**
45
	 * This is the dispatcher of smileys administration.
46
	 *
47
	 * @uses ManageSmileys language
48
	 * @uses ManageSmileys template
49
	 * @see AbstractController::action_index()
50
	 */
51
	public function action_index()
52
	{
53
		global $context, $txt, $modSettings;
54
55
		Txt::load('ManageSmileys');
56
		theme()->getTemplates()->load('ManageSmileys');
57
58
		$subActions = [
59
			'addsmiley' => [$this, 'action_addsmiley', 'permission' => 'manage_smileys'],
60
			'editicon' => [$this, 'action_editicon', 'enabled' => !empty($modSettings['messageIcons_enable']), 'permission' => 'manage_smileys'],
61
			'editicons' => [$this, 'action_editicon', 'enabled' => !empty($modSettings['messageIcons_enable']), 'permission' => 'manage_smileys'],
62
			'editsets' => [$this, 'action_edit', 'permission' => 'admin_forum'],
63
			'editsmileys' => [$this, 'action_editsmiley', 'permission' => 'manage_smileys'],
64
			'import' => [$this, 'action_edit', 'permission' => 'manage_smileys'],
65
			'modifyset' => [$this, 'action_edit', 'permission' => 'manage_smileys'],
66
			'modifysmiley' => [$this, 'action_editsmiley', 'permission' => 'manage_smileys'],
67
			'setorder' => [$this, 'action_setorder', 'permission' => 'manage_smileys'],
68
			'settings' => [$this, 'action_smileySettings_display', 'permission' => 'manage_smileys'],
69
			'install' => [$this, 'action_install', 'permission' => 'manage_smileys']
70
		];
71
72
		// Action controller
73
		$action = new Action('manage_smileys');
74
75
		// Set the smiley context.
76
		$this->_initSmileyContext();
77
78
		// Load up all the tabs...
79
		$context[$context['admin_menu_name']]['object']->prepareTabData([
80
			'title' => 'smileys_manage',
81
			'help' => 'smileys',
82
			'description' => 'smiley_settings_explain',
83
			'prefix' => 'smiley',
84
			'tabs' => [
85
				'editicons' => [
86
					'description' => $txt['icons_edit_icons_explain'],
87
				],
88
			],
89
		]);
90
91
		// Default the sub-action to 'edit smiley settings'. call integrate_sa_manage_smileys
92
		$subAction = $action->initialize($subActions, 'editsets');
93
94
		// Set up the template
95
		$context['page_title'] = $txt['smileys_manage'];
96
		$context['sub_action'] = $subAction;
97
98
		// Note: the 'enabled' flag already governs the visibility of the edit icons tab
99
		// in $subActions based on $modSettings['messageIcons_enable'].
100
		// No further mutation of the tab structure is required here.
101
102
		// Call the right function for this sub-action.
103
		$action->dispatch($subAction);
104
	}
105
106
	/**
107
	 * Sets our internal smiley context.
108
	 */
109
	private function _initSmileyContext(): void
110
	{
111
		global $modSettings;
112
113
		// Validate the smiley set name.
114
		$set_paths = explode(',', $modSettings['smiley_sets_known']);
115
		$set_names = explode("\n", $modSettings['smiley_sets_names']);
116
117
		foreach ($set_paths as $i => $set)
118
		{
119
			$this->_smiley_context[$set] = $set_names[$i];
120
		}
121
	}
122
123
	/**
124
	 * Displays and allows modification to smileys settings.
125
	 *
126
	 * @uses show_settings sub template
127
	 */
128
	public function action_smileySettings_display(): void
129
	{
130
		global $context;
131
132
		// initialize the form
133
		$settingsForm = new SettingsForm(SettingsForm::DB_ADAPTER);
134
		$settingsForm->setConfigVars($this->_settings());
135
136
		// For the basics of the settings.
137
		require_once(SUBSDIR . '/Smileys.subs.php');
138
		$context['sub_template'] = 'show_settings';
139
140
		// Finish up the form...
141
		$context['post_url'] = getUrl('admin', ['action' => 'admin', 'area' => 'smileys', 'save', 'sa' => 'settings']);
142
143
		// Saving the settings?
144
		if ($this->_req->hasQuery('save'))
145
		{
146
			checkSession();
147
148
			$this->_req->post->smiley_sets_default = $this->_req->getPost('smiley_sets_default', 'trim|strval', 'default');
149
150
			call_integration_hook('integrate_save_smiley_settings');
151
152
			// Save away
153
			$settingsForm->setConfigValues((array) $this->_req->post);
154
			$settingsForm->save();
155
156
			// Flush the cache so the new settings take effect
157
			$this->clearSmileyCache();
158
159
			redirectexit('action=admin;area=smileys;sa=settings');
160
		}
161
162
		$settingsForm->prepare();
163
	}
164
165
	/**
166
	 * Retrieve and return smiley administration settings.
167
	 */
168
	private function _settings()
169
	{
170
		global $txt, $modSettings, $context;
171
172
		// The directories...
173
		$context['smileys_dir'] = empty($modSettings['smileys_dir']) ? BOARDDIR . '/smileys' : $modSettings['smileys_dir'];
174
		$context['smileys_dir_found'] = FileFunctions::instance()->isDir($context['smileys_dir']);
175
176
		// All the settings for the page...
177
		$config_vars = [
178
			['title', 'settings'],
179
			['select', 'smiley_sets_default', $this->_smiley_context],
180
			['text', 'smileys_url', 40],
181
			['text', 'smileys_dir', 'invalid' => !$context['smileys_dir_found'], 40],
182
			'',
183
			// Message icons.
184
			['check', 'messageIcons_enable', 'subtext' => $txt['setting_messageIcons_enable_note']],
185
			// Inline permissions.
186
			'',
187
			['permissions', 'manage_smileys'],
188
		];
189
190
		call_integration_hook('integrate_modify_smiley_settings', [&$config_vars]);
191
192
		return $config_vars;
193 2
	}
194
195 2
	/**
196
	 * Clear the cache to avoid changes not immediately appearing
197
	 */
198 2
	protected function clearSmileyCache(): void
199 2
	{
200
		Cache::instance()->remove('parsing_smileys');
201
		Cache::instance()->remove('posting_smileys');
202
203 2
		$this->createCustomTagsFile();
204
	}
205
206 2
	/**
207 2
	 * For any :emoji: codes in the current set that have an image in the smile set directory,
208
	 * write the information to custom_tags.js so the emoji selector reflects the new or replacement image
209 2
	 *
210
	 * @return void
211 2
	 */
212 2
	protected function createCustomTagsFile(): void
213
	{
214 2
		global $context, $settings;
215
216
		$fileFunc = FileFunctions::instance();
217 2
		$custom = [];
218
		$smileys = list_getSmileys(0, 2000, 'code');
219 2
		foreach ($smileys as $smile)
220
		{
221
			if (preg_match('~:[\w-]{2,}:~u', $smile['code'], $match) === 1)
222
			{
223
				// If the emoji IS an image file in the currently selected set
224
				$filename = $context['smiley_dir'] . $smile['filename'] . '.' . $context['smiley_extension'];
225
				if ($fileFunc->fileExists($filename))
226
				{
227
					$custom[] = [
228
						'name' => trim($smile['code'], ':'),
229
						'key' => $smile['filename'],
230
						'type' => $context['smiley_extension']
231 2
					];
232
				}
233 2
			}
234
		}
235
236
		// Whatever we have, save it
237
		$header = '/*!
238
 * Auto-generated file, DO NOT manually edit
239
 *		
240
 * @package   ElkArte Forum
241
 * @copyright ElkArte Forum contributors
242
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
243
 *
244
 */
245
 
246
 let custom = [
247
 ';
248
		foreach ($custom as $emoji)
249
		{
250
			$header .= "\t" . '{name: \'' . $emoji['name'] . '\', key: \'' . $emoji['key'] . '\', type: \'' . $emoji['type'] . "'},\n";
251
		}
252
253
		$header .= '];';
254
255
		file_put_contents($settings['default_theme_dir'] . '/scripts/custom_tags.js', $header);
256
	}
257
258
	/**
259
	 * Return the smiley settings for use in admin search
260
	 */
261
	public function settings_search()
262
	{
263
		return $this->_settings();
264
	}
265
266
	/**
267
	 * List, add, remove, modify smiley sets.
268
	 *
269
	 * @event integrate_list_smiley_set_list
270
	 */
271
	public function action_edit(): void
272
	{
273
		global $modSettings, $context, $txt;
274
275
		require_once(SUBSDIR . '/Smileys.subs.php');
276
277
		// Set the right tab to be selected.
278
		$context[$context['admin_menu_name']]['current_subsection'] = 'editsets';
279
		$context['sub_template'] = $context['sub_action'];
280
281
		// Have they submitted a form?
282
		$this->_subActionSubmit();
283
284
		// Load all available smileysets...
285
		$this->loadSmileySets();
286
287
		// Importing any smileys from an existing set?
288
		$this->_subActionImport();
289
290
		// If we're modifying or adding a smileyset, some context info needs to be set.
291
		$this->_subActionModifySet();
292
293
		// This is our save haven.
294
		createToken('admin-mss', 'request');
295
296
		$listOptions = [
297
			'id' => 'smiley_set_list',
298
			'title' => $txt['smiley_sets'],
299
			'no_items_label' => $txt['smiley_sets_none'],
300
			'base_href' => getUrl('admin', ['action' => 'admin', 'area' => 'smileys', 'sa' => 'editsets']),
301
			'default_sort_col' => 'name',
302
			'get_items' => [
303
				'function' => 'list_getSmileySets',
304
			],
305
			'get_count' => [
306
				'function' => 'list_getNumSmileySets',
307
			],
308
			'columns' => [
309
				'default' => [
310
					'header' => [
311
						'value' => $txt['smiley_sets_default'],
312
						'class' => 'centertext',
313
					],
314
					'data' => [
315
						'function' => static fn($rowData) => $rowData['selected'] ? '<i class="icon i-check"></i>' : '',
316
						'class' => 'centertext',
317
					],
318
					'sort' => [
319
						'default' => 'selected',
320
						'reverse' => 'selected DESC',
321
					],
322
				],
323
				'name' => [
324
					'header' => [
325
						'value' => $txt['smiley_sets_name'],
326
					],
327
					'data' => [
328
						'db_htmlsafe' => 'name',
329
					],
330
					'sort' => [
331
						'default' => 'name',
332
						'reverse' => 'name DESC',
333
					],
334
				],
335
				'ext' => [
336
					'header' => [
337
						'value' => $txt['smiley_sets_ext'],
338
					],
339
					'data' => [
340
						'db_htmlsafe' => 'ext',
341
					],
342
					'sort' => [
343
						'default' => 'ext',
344
						'reverse' => 'ext DESC',
345
					],
346
				],
347
				'url' => [
348
					'header' => [
349
						'value' => $txt['smiley_sets_url'],
350
					],
351
					'data' => [
352
						'sprintf' => [
353
							'format' => $modSettings['smileys_url'] . '/<strong>%1$s</strong>/...',
354
							'params' => [
355
								'path' => true,
356
							],
357
						],
358
					],
359
					'sort' => [
360
						'default' => 'path',
361
						'reverse' => 'path DESC',
362
					],
363
				],
364
				'modify' => [
365
					'header' => [
366
						'value' => $txt['smiley_set_modify'],
367
					],
368
					'data' => [
369
						'sprintf' => [
370
							'format' => '<a href="' . getUrl('admin', ['action' => 'admin', 'area' => 'smileys', 'sa' => 'modifyset', 'set' => '']) . '%1$d">' . $txt['smiley_set_modify'] . '</a>',
371
							'params' => [
372
								'id' => true,
373
							],
374
						],
375
					],
376
				],
377
				'check' => [
378
					'header' => [
379
						'value' => '<input type="checkbox" onclick="invertAll(this, this.form);" class="input_check" />',
380
						'class' => 'centertext',
381
					],
382
					'data' => [
383
						'function' => static fn($rowData) => $rowData['id'] == 0 ? '' : sprintf('<input type="checkbox" name="smiley_set[%1$d]" class="input_check" />', $rowData['id']),
384
						'class' => 'centertext',
385
					],
386
				],
387
			],
388
			'form' => [
389
				'href' => getUrl('admin', ['action' => 'admin', 'area' => 'smileys', 'sa' => 'editsets']),
390
				'token' => 'admin-mss',
391
			],
392
			'additional_rows' => [
393
				[
394
					'position' => 'below_table_data',
395
					'class' => 'submitbutton',
396
					'value' => '
397
						<a class="linkbutton" href="' . getUrl('admin', ['action' => 'admin', 'area' => 'smileys', 'sa' => 'modifyset']) . '">' . $txt['smiley_sets_add'] . '</a> 
398
						<input type="submit" name="delete_set" value="' . $txt['smiley_sets_delete'] . '" onclick="return confirm(\'' . $txt['smiley_sets_confirm'] . '\');" />',
399
				],
400
			],
401
		];
402
403
		createList($listOptions);
404
	}
405
406
	/**
407
	 * Submitted a smiley form, determine what actions are required.
408
	 *
409
	 * - Handle deleting of a smiley set
410
	 * - Adding a new set
411
	 * - Modifying an existing set, such as setting it as the default
412
	 */
413
	private function _subActionSubmit(): void
414
	{
415
		global $context, $modSettings;
416
417
		if ($this->_req->hasPost('smiley_save') || $this->_req->hasPost('delete_set'))
418
		{
419
			// Security first
420
			checkSession();
421
			validateToken('admin-mss', 'request');
422
423
			// Delete selected smiley sets.
424
			$set = $this->_req->getPost('smiley_set', null, []);
425
			if (!is_array($set))
426
			{
427
				$set = [$set];
428
			}
429
			if ( !empty($set) && $this->_req->hasPost('delete_set'))
430
			{
431
				$set_paths = explode(',', $modSettings['smiley_sets_known']);
432
				$set_names = explode("\n", $modSettings['smiley_sets_names']);
433
				$set_extensions = explode(',', $modSettings['smiley_sets_extensions']);
434
				foreach ($set as $id => $val)
435
				{
436
					if (!isset($set_paths[$id], $set_names[$id]))
437
					{
438
						continue;
439
					}
440
					if (empty($id))
441
					{
442
						continue;
443
					}
444
					unset($set_paths[$id], $set_names[$id], $set_extensions[$id]);
445
				}
446
447
				// Update the modSettings with the new values
448
				updateSettings([
449
					'smiley_sets_known' => implode(',', $set_paths),
450
					'smiley_sets_names' => implode("\n", $set_names),
451
					'smiley_sets_extensions' => implode(',', $set_extensions),
452
					'smiley_sets_default' => in_array($modSettings['smiley_sets_default'], $set_paths, true) ? $modSettings['smiley_sets_default'] : $set_paths[0],
453
				]);
454
			}
455
			// Add a new smiley set.
456
			elseif ($this->_req->hasPost('add'))
457
			{
458
				$context['sub_action'] = 'modifyset';
459
			}
460
			// Create or modify a smiley set.
461
			elseif ($this->_req->hasPost('set'))
462
			{
463
				$set = $this->_req->getPost('set', 'intval', 0);
464
				$set_paths = explode(',', $modSettings['smiley_sets_known']);
465
				$set_names = explode("\n", $modSettings['smiley_sets_names']);
466
467
				// Create a new smiley set.
468
				if ($set === -1 && $this->_req->hasPost('smiley_sets_path'))
469
				{
470
					$setPath = $this->_req->getPost('smiley_sets_path', 'trim');
471
					if (in_array($setPath, $set_paths, true))
472
					{
473
						throw new Exception('smiley_set_already_exists', false);
474
					}
475
476
					// Determine the set type
477
					$setPath = $modSettings['smileys_dir'] . '/' . $setPath;
478
					$ext = getFirstImageExtensionInDir($setPath);
479
480
					updateSettings([
481
						'smiley_sets_known' => $modSettings['smiley_sets_known'] . ',' . $this->_req->getPost('smiley_sets_path', 'trim', ''),
482
						'smiley_sets_names' => $modSettings['smiley_sets_names'] . "\n" . $this->_req->getpost('smiley_sets_name', 'trim', ''),
483
						'smiley_sets_extensions' => $modSettings['smiley_sets_extensions'] . ',' . $ext ?? 'gif',
484
						'smiley_sets_default' => empty($this->_req->post->smiley_sets_default) ? $modSettings['smiley_sets_default'] : $this->_req->getPost('smiley_sets_path', 'trim', ''),
485
					]);
486
				}
487
				// Modify an existing smiley set.
488
				else
489
				{
490
					// Make sure the smiley set exists.
491
					if (!isset($set_paths[$set], $set_names[$set]))
492
					{
493
						throw new Exception('smiley_set_not_found', false);
494
					}
495
496
					// Make sure the path is not yet used by another smileyset.
497
					if (in_array($this->_req->post->smiley_sets_path, $set_paths, true) && $this->_req->post->smiley_sets_path !== $set_paths[$set])
498
					{
499
						throw new Exception('smiley_set_path_already_used', false);
500
					}
501
502
					$set_paths[$set] = $this->_req->post->smiley_sets_path;
503
					$set_names[$set] = $this->_req->post->smiley_sets_name;
504
					updateSettings([
505
						'smiley_sets_known' => implode(',', $set_paths),
506
						'smiley_sets_names' => implode("\n", $set_names),
507
						'smiley_sets_default' => empty($this->_req->post->smiley_sets_default) ? $modSettings['smiley_sets_default'] : $this->_req->post->smiley_sets_path
508
					]);
509
				}
510
511
				// The user might have checked to also import smileys.
512
				if (!empty($this->_req->post->smiley_sets_import))
513
				{
514
					$this->importSmileys($this->_req->post->smiley_sets_path);
515
				}
516
			}
517
518
			// Reset $context as the default set may have changed
519
			loadSmileyEmojiData();
520
521
			// No matter what, reset the cache
522
			$this->clearSmileyCache();
523
		}
524
	}
525
526
	/**
527
	 * A function to import new smileys from an existing directory into the database.
528
	 *
529
	 * @param string $smileyPath
530
	 *
531
	 * @throws Exception smiley_set_unable_to_import
532
	 */
533
	public function importSmileys(string $smileyPath): void
534
	{
535
		global $modSettings;
536
537
		require_once(SUBSDIR . '/Smileys.subs.php');
538
		$fileFunc = FileFunctions::instance();
539
540
		if (empty($modSettings['smileys_dir']) || !$fileFunc->isDir($modSettings['smileys_dir'] . '/' . $smileyPath))
541
		{
542
			throw new Exception('smiley_set_unable_to_import', false);
543
		}
544
545
		// Fetch all smileys in the directory
546
		$foundSmileys = $fileFunc->listTree($modSettings['smileys_dir'] . '/' . $smileyPath);
547
548
		// Exclude the smileys that are already in the database.
549
		$smileys = $this->_getUniqueSmileys($foundSmileys);
550
551
		$smiley_order = getMaxSmileyOrder();
552
553
		$new_smileys = [];
554
		foreach ($smileys as $smiley)
555
		{
556
			if (strlen($smiley) <= 48)
557
			{
558
				$new_smileys[] = [':' . strtok($smiley, '.') . ':', $smiley, strtok($smiley, '.'), 0, ++$smiley_order];
559
			}
560
		}
561
562
		if (!empty($new_smileys))
563
		{
564
			addSmiley($new_smileys);
565
566
			$this->clearSmileyCache();
567
		}
568
	}
569
570
	/**
571
	 * Returns smile names in a directory that *do not* exist in the DB
572
	 *
573
	 * @param array $foundSmileys array as returned from file functions listTree
574
	 * @return array
575
	 * @throws Exception
576
	 */
577
	private function _getUniqueSmileys(array $foundSmileys): array
578
	{
579
		global $context;
580
581
		$smileys = [];
582
		foreach ($foundSmileys as $smile)
583
		{
584
			if (in_array($smile['filename'], ['.', '..', '.htaccess', 'index.php'], true))
585
			{
586
				continue;
587
			}
588
589
			// Get the name and extension
590
			$filename = pathinfo($smile['filename'], PATHINFO_FILENAME);
591
			if (in_array(strtolower(pathinfo($smile['filename'], PATHINFO_EXTENSION)), $this->_smiley_types, true))
0 ignored issues
show
Bug introduced by
It seems like pathinfo($smile['filenam...ler\PATHINFO_EXTENSION) can also be of type array; however, parameter $string of strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

591
			if (in_array(strtolower(/** @scrutinizer ignore-type */ pathinfo($smile['filename'], PATHINFO_EXTENSION)), $this->_smiley_types, true))
Loading history...
592
			{
593
				$smileys[strtolower($filename)] = $filename;
594
			}
595
		}
596
597
		if (empty($smileys))
598
		{
599
			throw new Exception('smiley_set_dir_not_found', false, [$context['current_set']['name']]);
600
		}
601
602
		// Exclude the smileys that are already in the database.
603
		$duplicates = smileyExists($smileys);
604
		foreach ($duplicates as $duplicate)
605
		{
606
			if (isset($smileys[strtolower($duplicate)]))
607
			{
608
				unset($smileys[strtolower($duplicate)]);
609
			}
610
		}
611
612
		return $smileys;
613
	}
614
615
	/**
616
	 * Importing smileys from an existing smiley set
617
	 */
618
	private function _subActionImport(): void
619
	{
620
		global $context;
621
622
		// Importing any smileys from an existing set?
623
		if ($context['sub_action'] === 'import')
624
		{
625
			checkSession('get');
626
			validateToken('admin-mss', 'request');
627
628
			$set = $this->_req->getQuery('set', 'intval', -1);
629
630
			// Sanity check - then import.
631
			if (isset($context['smiley_sets'][$set]))
632
			{
633
				$this->importSmileys(un_htmlspecialchars($context['smiley_sets'][$set]['path']));
634
			}
635
636
			// Force the process to continue.
637
			$context['sub_action'] = 'modifyset';
638
			$context['sub_template'] = 'modifyset';
639
		}
640
	}
641
642
	/**
643
	 * If we're modifying or adding a smileyset, or if we imported from another
644
	 * set, then some context info needs to be set.
645
	 */
646
	private function _subActionModifySet(): void
647
	{
648
		global $context, $txt, $modSettings;
649
650
		$fileFunc = FileFunctions::instance();
651
652
		if ($context['sub_action'] === 'modifyset')
653
		{
654
			$set = $this->_req->getQuery('set', 'intval', -1);
655
			if ($set === -1 || !isset($context['smiley_sets'][$set]))
656
			{
657
				$context['current_set'] = [
658
					'id' => '-1',
659
					'path' => '',
660
					'name' => '',
661
					'ext' => '',
662
					'selected' => false,
663
					'is_new' => true,
664
				];
665
			}
666
			else
667
			{
668
				$context['current_set'] = &$context['smiley_sets'][$set];
669
				$context['current_set']['is_new'] = false;
670
				$context['smileys_dir'] = empty($modSettings['smileys_dir']) ? BOARDDIR . '/smileys' : $modSettings['smileys_dir'];
671
				$context['selected_set'] = $modSettings['smiley_sets_default'];
672
673
				// Calculate whether there are any smileys in the directory that can be imported.
674
				if (!empty($modSettings['smileys_dir'])
675
					&& $fileFunc->isDir($modSettings['smileys_dir'] . '/' . $context['current_set']['path']))
676
				{
677
					$foundSmileys = $fileFunc->listTree($modSettings['smileys_dir'] . '/' . $context['current_set']['path']);
678
					$smileys = $this->_getUniqueSmileys($foundSmileys);
679
					$context['current_set']['can_import'] = count($smileys);
680
681
					// Set up this string to look nice.
682
					$txt['smiley_set_import_multiple'] = sprintf($txt['smiley_set_import_multiple'], $context['current_set']['can_import']);
683
				}
684
			}
685
686
			// Retrieve all potential smiley set directories.
687
			$context['smiley_set_dirs'] = [];
688
			if (!empty($modSettings['smileys_dir']) && $fileFunc->isDir($modSettings['smileys_dir']))
689
			{
690
				// Do not include our emoji directories
691
				$disallow = ['.', '..', 'open-moji', 'tw-emoji', 'noto-emoji'];
692
693
				$dir = dir($modSettings['smileys_dir']);
694
				while (($entry = $dir->read()) !== false)
695
				{
696
					if (!in_array($entry, $disallow, true)
697
						&& $fileFunc->isDir($modSettings['smileys_dir'] . '/' . $entry))
698
					{
699
						// What kind of set are we dealing with?
700
						$setPath = $modSettings['smileys_dir'] . '/' . $entry;
701
						$ext = getFirstImageExtensionInDir($setPath);
702
703
						$context['smiley_set_dirs'][] = [
704
							'id' => $entry,
705
							'path' => $modSettings['smileys_dir'] . '/' . $entry,
706
							'ext' => $ext,
707
							'selectable' => $entry === $context['current_set']['path'] || !in_array($entry, explode(',', $modSettings['smiley_sets_known']), true),
708
							'current' => $entry === $context['current_set']['path'],
709
						];
710
					}
711
				}
712
713
				$dir->close();
714
			}
715
		}
716
	}
717
718
	/**
719
	 * Add a smiley, that's right.
720
	 */
721
	public function action_addsmiley(): void
722
	{
723
		global $modSettings, $context, $txt;
724
725
		$fileFunc = FileFunctions::instance();
726
		require_once(SUBSDIR . '/Smileys.subs.php');
727
728
		// Get a list of all known smiley sets.
729
		$context['smileys_dir'] = empty($modSettings['smileys_dir']) ? BOARDDIR . '/smileys' : $modSettings['smileys_dir'];
730
		$context['sub_template'] = 'addsmiley';
731
732
		$this->loadSmileySets();
733
734
		// Submitting a form?
735
		if (isset($this->_req->post->{$context['session_var']}, $this->_req->post->smiley_code))
736
		{
737
			checkSession();
738
739
			// Some useful arrays... types we allow - and ports we don't!
740
			$disabledFiles = ['con', 'com1', 'com2', 'com3', 'com4', 'prn', 'aux', 'lpt1', '.htaccess', 'index.php'];
741
742
			$this->_req->post->smiley_code = $this->_req->getPost('smiley_code', 'Util::htmltrim', '');
743
			$this->_req->post->smiley_filename = $this->_req->getPost('smiley_filename', 'Util::htmltrim', '');
744
			$this->_req->post->smiley_location = $this->_req->getPost('smiley_location', 'intval', 0);
745
			$this->_req->post->smiley_location = min(max($this->_req->post->smiley_location, 0), 2);
746
747
			// Make sure some code was entered.
748
			if (empty($this->_req->post->smiley_code))
749
			{
750
				throw new Exception('smiley_has_no_code', false);
751
			}
752
753
			// Check whether the new code has duplicates. It should be unique.
754
			if (validateDuplicateSmiley($this->_req->post->smiley_code))
755
			{
756
				throw new Exception('smiley_not_unique', false);
757
			}
758
759
			// If we are uploading - check all the smiley sets are writable!
760
			if ($this->_req->post->method !== 'existing')
761
			{
762
				$writeErrors = [];
763
				foreach ($context['smiley_sets'] as $set)
764
				{
765
					if (!$fileFunc->isWritable($context['smileys_dir'] . '/' . un_htmlspecialchars($set['path'])))
766
					{
767
						$writeErrors[] = $set['path'];
768
					}
769
				}
770
771
				if (!empty($writeErrors))
772
				{
773
					throw new Exception('smileys_upload_error_notwritable', true, [implode(', ', $writeErrors)]);
774
				}
775
			}
776
777
			// Uploading just one smiley for all of them?
778
			if (isset($this->_req->post->sameall, $_FILES['uploadSmiley']['name']) && $_FILES['uploadSmiley']['name'] != '')
779
			{
780
				if (!is_uploaded_file($_FILES['uploadSmiley']['tmp_name'])
781
					|| (ini_get('open_basedir') === '' && !$fileFunc->fileExists($_FILES['uploadSmiley']['tmp_name'])))
782
				{
783
					throw new Exception('smileys_upload_error');
784
				}
785
786
				// Sorry, no spaces, dots, or anything else but letters allowed.
787
				$_FILES['uploadSmiley']['name'] = preg_replace(['/\s/', '/\.[\.]+/', '/[^\w_\.\-]/'], ['_', '.', ''], $_FILES['uploadSmiley']['name']);
788
789
				// We only allow image files - it's THAT simple - no messing around here...
790
				if (!in_array(strtolower(pathinfo($_FILES['uploadSmiley']['name'], PATHINFO_EXTENSION)), $this->_smiley_types))
791
				{
792
					throw new Exception('smileys_upload_error_types', false, [implode(', ', $this->_smiley_types)]);
793
				}
794
795
				// We only need the filename...
796
				$destName = basename($_FILES['uploadSmiley']['name']);
797
798
				// Make sure they aren't trying to upload a nasty file - for their own good here!
799
				if (in_array(strtolower($destName), $disabledFiles))
800
				{
801
					throw new Exception('smileys_upload_error_illegal');
802
				}
803
804
				// Check if the file already exists... and if not, move it to EVERY smiley set directory.
805
				$i = 0;
806
807
				// Keep going until we find a set the file doesn't exist in. (or maybe it exists in all of them?)
808
				while (isset($context['smiley_sets'][$i])
809
					&& $fileFunc->fileExists($context['smileys_dir'] . '/' . un_htmlspecialchars($context['smiley_sets'][$i]['path']) . '/' . $destName))
810
				{
811
					$i++;
812
				}
813
814
				// Okay, we're going to put the smiley right here, since it's not there yet!
815
				if (isset($context['smiley_sets'][$i]['path']))
816
				{
817
					$smileyLocation = $context['smileys_dir'] . '/' . un_htmlspecialchars($context['smiley_sets'][$i]['path']) . '/' . $destName;
818
					move_uploaded_file($_FILES['uploadSmiley']['tmp_name'], $smileyLocation);
819
					$fileFunc->chmod($smileyLocation);
820
821
					// Now, we want to move it from there to all the other sets.
822
					for ($n = count($context['smiley_sets']); $i < $n; $i++)
823
					{
824
						$currentPath = $context['smileys_dir'] . '/' . un_htmlspecialchars($context['smiley_sets'][$i]['path']) . '/' . $destName;
825
826
						// The file is already there!  Don't overwrite it!
827
						if ($fileFunc->fileExists($currentPath))
828
						{
829
							continue;
830
						}
831
832
						// Okay, so copy the first one we made to here.
833
						copy($smileyLocation, $currentPath);
834
						$fileFunc->chmod($currentPath);
835
					}
836
				}
837
838
				// Finally, make sure it's saved correctly!
839
				$this->_req->post->smiley_filename = $destName;
840
			}
841
			// What about uploading several files?
842
			elseif ($this->_req->post->method !== 'existing')
843
			{
844
				$newName = '';
845
				foreach ($_FILES as $file)
846
				{
847
					if ($file['name'] === '')
848
					{
849
						throw new Exception('smileys_upload_error_blank', false);
850
					}
851
852
					if (empty($newName))
853
					{
854
						$newName = basename($file['name']);
855
					}
856
					elseif (basename($file['name']) !== $newName)
857
					{
858
						throw new Exception('smileys_upload_error_name', false);
859
					}
860
				}
861
862
				foreach ($context['smiley_sets'] as $set)
863
				{
864
					$set['name'] = un_htmlspecialchars($set['name']);
865
					$set['path'] = un_htmlspecialchars($set['path']);
866
867
					if (!isset($_FILES['individual_' . $set['name']]['name']) || $_FILES['individual_' . $set['name']]['name'] === '')
868
					{
869
						continue;
870
					}
871
872
					// Got one...
873
					if (!is_uploaded_file($_FILES['individual_' . $set['name']]['tmp_name']) || (ini_get('open_basedir') === ''
874
							&& !$fileFunc->fileExists($_FILES['individual_' . $set['name']]['tmp_name'])))
875
					{
876
						throw new Exception('smileys_upload_error');
877
					}
878
879
					// Sorry, no spaces, dots, or anything else but letters allowed.
880
					$_FILES['individual_' . $set['name']]['name'] = preg_replace(['/\s/', '/\.[\.]+/', '/[^\w_\.\-]/'], ['_', '.', ''], $_FILES['individual_' . $set['name']]['name']);
881
882
					// We only allow image files - it's THAT simple - no messing around here...
883
					if (!in_array(strtolower(pathinfo($_FILES['individual_' . $set['name']]['name'], PATHINFO_EXTENSION)), $this->_smiley_types))
884
					{
885
						throw new Exception('smileys_upload_error_types', false, [implode(', ', $this->_smiley_types)]);
886
					}
887
888
					// We only need the filename...
889
					$destName = basename($_FILES['individual_' . $set['name']]['name']);
890
891
					// Make sure they aren't trying to upload a nasty file - for their own good here!
892
					if (in_array(strtolower($destName), $disabledFiles))
893
					{
894
						throw new Exception('smileys_upload_error_illegal');
895
					}
896
897
					// If the file exists - ignore it.
898
					$smileyLocation = $context['smileys_dir'] . '/' . $set['path'] . '/' . $destName;
899
					if ($fileFunc->fileExists($smileyLocation))
900
					{
901
						continue;
902
					}
903
904
					// Finally - move the image!
905
					move_uploaded_file($_FILES['individual_' . $set['name']]['tmp_name'], $smileyLocation);
906
					$fileFunc->chmod($smileyLocation);
907
908
					// Should always be saved correctly!
909
					$this->_req->post->smiley_filename = $destName;
910
				}
911
			}
912
913
			// Also make sure a filename was given.
914
			if (empty($this->_req->post->smiley_filename))
915
			{
916
				throw new Exception('smiley_has_no_filename', false);
917
			}
918
919
			// Find the position on the right.
920
			$smiley_order = '0';
921
			if ($this->_req->post->smiley_location != 1)
922
			{
923
				$this->_req->post->smiley_location = (int) $this->_req->post->smiley_location;
924
				$smiley_order = nextSmileyLocation($this->_req->post->smiley_location);
925
926
				if (empty($smiley_order))
927
				{
928
					$smiley_order = '0';
929
				}
930
			}
931
932
			$param = [
933
				$this->_req->post->smiley_code,
934
				$this->_req->post->smiley_filename,
935
				$this->_req->post->smiley_description,
936
				(int) $this->_req->post->smiley_location,
937
				$smiley_order,
938
			];
939
			addSmiley($param);
940
941
			$this->clearSmileyCache();
942
943
			// No errors? Out of here!
944
			redirectexit('action=admin;area=smileys;sa=editsmileys');
945
		}
946
947
		$context['selected_set'] = $modSettings['smiley_sets_default'];
948
949
		// Get all possible filenames for the smileys.
950
		$context['filenames'] = $this->getAllPossibleFilenamesForTheSmileys($context['smiley_sets']);
951
952
		// Create a new smiley from scratch.
953
		$context['filenames'] = array_values($context['filenames']);
954
		$context['current_smiley'] = [
955
			'id' => 0,
956
			'code' => '',
957
			'filename' => $context['filenames'][0]['id'],
958
			'description' => $txt['smileys_default_description'],
959
			'location' => 0,
960
			'is_new' => true,
961
		];
962
	}
963
964
	/**
965
	 * Add, remove, edit smileys.
966
	 *
967
	 * @event integrate_list_smiley_list
968
	 */
969
	public function action_editsmiley(): void
970
	{
971
		global $modSettings, $context, $txt;
972
973
		$fileFunc = FileFunctions::instance();
974
		require_once(SUBSDIR . '/Smileys.subs.php');
975
976
		// Force the correct tab to be displayed.
977
		$context[$context['admin_menu_name']]['current_subsection'] = 'editsmileys';
978
		$context['sub_template'] = $context['sub_action'];
979
980
		// Submitting a form?
981
		if (isset($this->_req->post->smiley_save) || isset($this->_req->post->smiley_action))
982
		{
983
			checkSession();
984
985
			// Changing the selected smileys?
986
			if (isset($this->_req->post->smiley_action) && !empty($this->_req->post->checked_smileys))
987
			{
988
				foreach ($this->_req->post->checked_smileys as $id => $smiley_id)
989
				{
990
					$this->_req->post->checked_smileys[$id] = (int) $smiley_id;
991
				}
992
993
				if ($this->_req->post->smiley_action === 'delete')
994
				{
995
					deleteSmileys($this->_req->post->checked_smileys);
996
				}
997
				// Changing the status of the smiley?
998
				else
999
				{
1000
					// Check it's a valid type.
1001
					$displayTypes = [
1002
						'post' => 0,
1003
						'hidden' => 1,
1004
						'popup' => 2
1005
					];
1006
					if (isset($displayTypes[$this->_req->post->smiley_action]))
1007
					{
1008
						updateSmileyDisplayType($this->_req->post->checked_smileys, $displayTypes[$this->_req->post->smiley_action]);
1009
					}
1010
				}
1011
			}
1012
			// Create/modify a smiley.
1013
			elseif (isset($this->_req->post->smiley))
1014
			{
1015
				$this->_req->post->smiley = (int) $this->_req->post->smiley;
1016
1017
				// Is it a deletion?
1018
				if (!empty($this->_req->post->deletesmiley))
1019
				{
1020
					deleteSmileys([$this->_req->post->smiley]);
1021
				}
1022
				// Otherwise an edit.
1023
				else
1024
				{
1025
					$this->_req->post->smiley_code = $this->_req->getPost('smiley_code', 'Util::htmltrim', '');
1026
					$this->_req->post->smiley_filename = $this->_req->getPost('smiley_filename', 'Util::htmltrim', '');
1027
					$this->_req->post->smiley_location = empty($this->_req->post->smiley_location)
1028
						|| $this->_req->post->smiley_location > 2
1029
						|| $this->_req->post->smiley_location < 0 ? 0 : (int) $this->_req->post->smiley_location;
1030
1031
					// Make sure some code was entered.
1032
					if (empty($this->_req->post->smiley_code))
1033
					{
1034
						throw new Exception('smiley_has_no_code', false);
1035
					}
1036
1037
					// Also make sure a filename was given.
1038
					if (empty($this->_req->post->smiley_filename))
1039
					{
1040
						throw new Exception('smiley_has_no_filename', false);
1041
					}
1042
1043
					// Check whether the new code has duplicates. It should be unique.
1044
					if (validateDuplicateSmiley($this->_req->post->smiley_code, $this->_req->post->smiley))
1045
					{
1046
						throw new Exception('smiley_not_unique', false);
1047
					}
1048
1049
					$param = [
1050
						'smiley_location' => $this->_req->post->smiley_location,
1051
						'smiley' => $this->_req->post->smiley,
1052
						'smiley_code' => $this->_req->post->smiley_code,
1053
						'smiley_filename' => $this->_req->post->smiley_filename,
1054
						'smiley_description' => $this->_req->post->smiley_description,
1055
					];
1056
					updateSmiley($param);
1057
				}
1058
			}
1059
1060
			$this->clearSmileyCache();
1061
		}
1062
1063
		// Load all known smiley sets.
1064
		$this->loadSmileySets();
1065
1066
		// Prepare an overview of all (custom) smileys.
1067
		if ($context['sub_action'] === 'editsmileys')
1068
		{
1069
			theme()->addJavascriptVar([
1070
				'txt_remove' => JavaScriptEscape($txt['smileys_confirm']),
1071
			]);
1072
1073
			// Determine the language-specific sort order of smiley locations.
1074
			$smiley_locations = [
1075
				$txt['smileys_location_form'],
1076
				$txt['smileys_location_hidden'],
1077
				$txt['smileys_location_popup'],
1078
			];
1079
			asort($smiley_locations);
1080
1081
			// Create a list of options for selecting smiley sets.
1082
			$smileyset_option_list = '
1083
				<select id="set" name="set" onchange="changeSet(this.options[this.selectedIndex].value);">';
1084
1085
			foreach ($context['smiley_sets'] as $smiley_set)
1086
			{
1087
				$smileyset_option_list .= '
1088
					<option data-ext="' . $smiley_set['ext'] . '" value="' . $smiley_set['path'] . '"' . ($modSettings['smiley_sets_default'] === $smiley_set['path'] ? ' selected="selected"' : '') . '>' . $smiley_set['name'] . '</option>';
1089
			}
1090
1091
			$smileyset_option_list .= '
1092
				</select>';
1093
1094
			$listOptions = [
1095
				'id' => 'smiley_list',
1096
				'title' => $txt['smileys_edit'],
1097
				'items_per_page' => 40,
1098
				'base_href' => getUrl('admin', ['action' => 'admin', 'area' => 'smileys', 'sa' => 'editsmileys']),
1099
				'default_sort_col' => 'filename',
1100
				'get_items' => [
1101
					'function' => 'list_getSmileys',
1102
				],
1103
				'get_count' => [
1104
					'function' => 'list_getNumSmileys',
1105
				],
1106
				'no_items_label' => $txt['smileys_no_entries'],
1107
				'columns' => [
1108
					'picture' => [
1109
						'data' => [
1110
							'function' => static fn($rowData) => '
1111
								<a href="' . getUrl('admin', ['action' => 'admin', 'area' => 'smileys', 'sa' => 'modifysmiley', 'smiley' => '']) . $rowData['id_smiley'] . '">
1112
									<img class="smiley" src="' . $context['smiley_path'] . $rowData['filename'] . '.' . $context['smiley_extension'] . '" alt="' . $rowData['description'] . '" id="smiley' . $rowData['id_smiley'] . '" />
1113
									<input type="hidden" name="smileys[' . $rowData['id_smiley'] . '][filename]" value="' . $rowData['filename'] . '" />
1114
								</a>'
1115
						],
1116
						'class' => 'imagecolumn',
1117
					],
1118
					'code' => [
1119
						'header' => [
1120
							'value' => $txt['smileys_code'],
1121
						],
1122
						'data' => [
1123
							'db_htmlsafe' => 'code',
1124
						],
1125
						'sort' => [
1126
							'default' => 'code',
1127
							'reverse' => 'code DESC',
1128
						],
1129
					],
1130
					'filename' => [
1131
						'header' => [
1132
							'value' => $txt['smileys_filename'],
1133
						],
1134
						'data' => [
1135
							'db_htmlsafe' => 'filename',
1136
						],
1137
						'sort' => [
1138
							'default' => 'filename',
1139
							'reverse' => 'filename DESC',
1140
						],
1141
					],
1142
					'location' => [
1143
						'header' => [
1144
							'value' => $txt['smileys_location'],
1145
						],
1146
						'data' => [
1147
							'function' => static fn($rowData) => $smiley_locations[$rowData['hidden']],
1148
						],
1149
						'sort' => [
1150
							'default' => 'hidden',
1151
							'reverse' => 'hidden DESC',
1152
						],
1153
					],
1154
					'description' => [
1155
						'header' => [
1156
							'value' => $txt['smileys_description'],
1157
						],
1158
						'data' => [
1159
							'function' => static function ($rowData) use ($fileFunc) {
1160
								global $context, $txt, $modSettings;
1161
1162
								if (empty($modSettings['smileys_dir']) || !$fileFunc->isDir($modSettings['smileys_dir']))
1163
								{
1164
									return htmlspecialchars($rowData['description'], ENT_COMPAT, 'UTF-8');
1165
								}
1166
1167
								// Check if there are smileys missing in some sets.
1168
								$missing_sets = [];
1169
								$found_replacement = '';
1170
								foreach ($context['smiley_sets'] as $smiley_set)
1171
								{
1172
									$filename = $rowData['filename'] . '.' . $smiley_set['ext'];
1173
									if (!$fileFunc->fileExists($modSettings['smileys_dir'] . '/' . $smiley_set['path'] . '/' . $filename))
1174
									{
1175
										$missing_sets[] = $smiley_set['path'];
1176
										if (possibleSmileEmoji($rowData, $modSettings['smileys_dir'] . '/' . $smiley_set['path'], $smiley_set['ext']))
1177
										{
1178
											$found_replacement = $rowData['emoji'] . '.svg';
1179
										}
1180
									}
1181
								}
1182
1183
								$description = htmlspecialchars($rowData['description'], ENT_COMPAT, 'UTF-8');
1184
1185
								if (!empty($missing_sets))
1186
								{
1187
									$description .= '
1188
1189
										<p class="smalltext">
1190
											<strong>' . $txt['smileys_not_found_in_set'] . '</strong> ' . implode(', ', $missing_sets);
1191
1192
									if ($found_replacement !== '' && $found_replacement !== '0')
1193
									{
1194
										$description .= '<br />' . sprintf($txt['smileys_emoji_found'], '<img class="smiley emoji" src="' . $context['emoji_path'] . $found_replacement . '" /> ');
1195
									}
1196
1197
									$description .= '
1198
										</p>';
1199
								}
1200
1201
								return $description;
1202
							},
1203
						],
1204
						'sort' => [
1205
							'default' => 'description',
1206
							'reverse' => 'description DESC',
1207
						],
1208
					],
1209
					'modify' => [
1210
						'header' => [
1211
							'value' => $txt['smileys_modify'],
1212
							'class' => 'centertext',
1213
						],
1214
						'data' => [
1215
							'sprintf' => [
1216
								'format' => '<a href="' . getUrl('admin', ['action' => 'admin', 'area' => 'smileys', 'sa' => 'modifysmiley', 'smiley' => '']) . '%1$d">' . $txt['smileys_modify'] . '</a>',
1217
								'params' => [
1218
									'id_smiley' => false,
1219
								],
1220
							],
1221
							'class' => 'centertext',
1222
						],
1223
					],
1224
					'check' => [
1225
						'header' => [
1226
							'value' => '<input type="checkbox" onclick="invertAll(this, this.form);" class="input_check" />',
1227
							'class' => 'centertext',
1228
						],
1229
						'data' => [
1230
							'sprintf' => [
1231
								'format' => '<input type="checkbox" name="checked_smileys[]" value="%1$d" class="input_check" />',
1232
								'params' => [
1233
									'id_smiley' => false,
1234
								],
1235
							],
1236
							'class' => 'centertext',
1237
						],
1238
					],
1239
				],
1240
				'form' => [
1241
					'href' => getUrl('admin', ['action' => 'admin', 'area' => 'smileys', 'sa' => 'editsmileys']),
1242
					'name' => 'smileyForm',
1243
				],
1244
				'additional_rows' => [
1245
					[
1246
						'position' => 'above_column_headers',
1247
						'value' => $txt['smiley_sets'] . $smileyset_option_list,
1248
						'class' => 'flow_flex_right',
1249
					],
1250
					[
1251
						'position' => 'below_table_data',
1252
						'value' => '
1253
							<select name="smiley_action" onchange="makeChanges(this.value);">
1254
								<option value="-1">' . $txt['smileys_with_selected'] . ':</option>
1255
								<option value="-1">--------------</option>
1256
								<option value="hidden">' . $txt['smileys_make_hidden'] . '</option>
1257
								<option value="post">' . $txt['smileys_show_on_post'] . '</option>
1258
								<option value="popup">' . $txt['smileys_show_on_popup'] . '</option>
1259
								<option value="delete">' . $txt['smileys_remove'] . '</option>
1260
							</select>
1261
							<noscript>
1262
								<input type="submit" name="perform_action" value="' . $txt['go'] . '" class="right_submit" />
1263
							</noscript>',
1264
						'class' => 'righttext',
1265
					],
1266
				],
1267
			];
1268
1269
			createList($listOptions);
1270
1271
			// The list is the only thing to show, so make it the main template.
1272
			$context['default_list'] = 'smiley_list';
1273
			$context['sub_template'] = 'show_list';
1274
		}
1275
		// Modifying smileys.
1276
		elseif ($context['sub_action'] === 'modifysmiley')
1277
		{
1278
			// Get a list of all known smiley sets.
1279
			$context['smileys_dir'] = empty($modSettings['smileys_dir']) ? BOARDDIR . '/smileys' : $modSettings['smileys_dir'];
1280
			$context['selected_set'] = $modSettings['smiley_sets_default'];
1281
1282
			$this->loadSmileySets();
1283
1284
			// Get all possible filenames for the smileys.
1285
			$context['filenames'] = $this->getAllPossibleFilenamesForTheSmileys($context['smiley_sets']);
1286
1287
			$thisSmiley = $this->_req->getQuery('smiley', 'intval', 0);
1288
			$context['current_smiley'] = getSmiley($thisSmiley);
1289
			$context['current_smiley']['code'] = htmlspecialchars($context['current_smiley']['code'], ENT_COMPAT, 'UTF-8');
1290
			$context['current_smiley']['filename'] = htmlspecialchars($context['current_smiley']['filename'], ENT_COMPAT, 'UTF-8');
1291
			$context['current_smiley']['description'] = htmlspecialchars($context['current_smiley']['description'], ENT_COMPAT, 'UTF-8');
1292
1293
			if (isset($context['filenames'][strtolower($context['current_smiley']['filename'])]))
1294
			{
1295
				$context['filenames'][strtolower($context['current_smiley']['filename'])]['selected'] = true;
1296
			}
1297
		}
1298
	}
1299
1300
	/**
1301
	 * Allows editing the message icons.
1302
	 *
1303
	 * @event integrate_list_message_icon_list
1304
	 */
1305
	public function action_editicon(): void
1306
	{
1307
		global $context, $settings, $txt;
1308
1309
		$fileFunc = FileFunctions::instance();
1310
		require_once(SUBSDIR . '/MessageIcons.subs.php');
1311
1312
		// Get a list of icons.
1313
		$context['icons'] = fetchMessageIconsDetails();
1314
1315
		// Submitting a form?
1316
		if (isset($this->_req->post->icons_save))
1317
		{
1318
			checkSession();
1319
1320
			// Deleting icons?
1321
			if (isset($this->_req->post->delete) && !empty($this->_req->post->checked_icons))
1322
			{
1323
				$deleteIcons = [];
1324
				foreach ($this->_req->post->checked_icons as $icon)
1325
				{
1326
					$deleteIcons[] = (int) $icon;
1327
				}
1328
1329
				// Do the actual delete!
1330
				deleteMessageIcons($deleteIcons);
1331
			}
1332
			// Editing/Adding an icon?
1333
			elseif ($context['sub_action'] === 'editicon' && $this->_req->hasQuery('icon'))
1334
			{
1335
				$iconId = $this->_req->getQuery('icon', 'intval', 0);
1336
1337
				// Do some preparation with the data... like check the icon exists *somewhere*
1338
				if (str_contains($this->_req->post->icon_filename, '.png'))
1339
				{
1340
					$this->_req->post->icon_filename = substr($this->_req->post->icon_filename, 0, -4);
1341
				}
1342
1343
				if (!$fileFunc->fileExists($settings['default_theme_dir'] . '/images/post/' . $this->_req->post->icon_filename . '.png'))
1344
				{
1345
					throw new Exception('icon_not_found', false);
1346
				}
1347
1348
				// There is a 16-character limit on message icons...
1349
				if (strlen($this->_req->post->icon_filename) > 16)
1350
				{
1351
					throw new Exception('icon_name_too_long', false);
1352
				}
1353
1354
				if ((int) $this->_req->post->icon_location === $iconId && $iconId !== 0)
1355
				{
1356
					throw new Exception('icon_after_itself', false);
1357
				}
1358
1359
				// First, do the sorting... if this is an edit, reduce the order of everything after it by one ;)
1360
				if ($iconId !== 0)
1361
				{
1362
					$oldOrder = $context['icons'][$iconId]['true_order'];
1363
					foreach ($context['icons'] as $id => $data)
1364
					{
1365
						if ($data['true_order'] > $oldOrder)
1366
						{
1367
							$context['icons'][$id]['true_order']--;
1368
						}
1369
					}
1370
				}
1371
1372
				// If there are no existing icons and this is a new, set the id to 1 (mainly for non-mysql)
1373
				if ($iconId === 0 && empty($context['icons']))
1374
				{
1375
					$iconId = 1;
1376
				}
1377
1378
				// Get the new order.
1379
				$newOrder = $this->_req->post->icon_location == 0 ? 0 : $context['icons'][$this->_req->post->icon_location]['true_order'] + 1;
1380
1381
				// Do the same, but with the one that used to be after this icon, done to avoid conflict.
1382
				foreach ($context['icons'] as $id => $data)
1383
				{
1384
					if ($data['true_order'] >= $newOrder)
1385
					{
1386
						$context['icons'][$id]['true_order']++;
1387
					}
1388
				}
1389
1390
				// Finally, set the current icon's position!
1391
				$context['icons'][$iconId]['true_order'] = $newOrder;
1392
1393
				// Simply replace the existing data for the other bits.
1394
				$context['icons'][$iconId]['title'] = $this->_req->post->icon_description;
1395
				$context['icons'][$iconId]['filename'] = $this->_req->post->icon_filename;
1396
				$context['icons'][$iconId]['board_id'] = (int) $this->_req->post->icon_board;
1397
1398
				// Do a huge replacement ;)
1399
				$iconInsert = [];
1400
				$iconInsert_new = [];
1401
				foreach ($context['icons'] as $id => $icon)
1402
				{
1403
					if ($id != 0)
1404
					{
1405
						$iconInsert[] = [$id, $icon['board_id'], $icon['title'], $icon['filename'], $icon['true_order']];
1406
					}
1407
					else
1408
					{
1409
						$iconInsert_new[] = [$icon['board_id'], $icon['title'], $icon['filename'], $icon['true_order']];
1410
					}
1411
				}
1412
1413
				updateMessageIcon($iconInsert);
1414
1415
				if (!empty($iconInsert_new))
1416
				{
1417
					addMessageIcon($iconInsert_new);
1418
1419
					// Flush the cache so the changes are reflected
1420
					Cache::instance()->remove('posting_icons-' . (int) $this->_req->post->icon_board);
1421
				}
1422
			}
1423
1424
			// Unless we're adding a new thing, we'll escape
1425
			if (!isset($this->_req->post->add))
1426
			{
1427
				Cache::instance()->remove('posting_icons-0');
1428
				redirectexit('action=admin;area=smileys;sa=editicons');
1429
			}
1430
		}
1431
1432
		$context[$context['admin_menu_name']]['current_subsection'] = 'editicons';
1433
		$token = createToken('admin-sort');
1434
		$listOptions = [
1435
			'id' => 'message_icon_list',
1436
			'title' => $txt['icons_edit_message_icons'],
1437
			'sortable' => true,
1438
			'base_href' => getUrl('admin', ['action' => 'admin', 'area' => 'smileys', 'sa' => 'editicons']),
1439
			'get_items' => [
1440
				'function' => fn() => $this->list_fetchMessageIconsDetails(),
1441
			],
1442
			'no_items_label' => $txt['icons_no_entries'],
1443
			'columns' => [
1444
				'icon' => [
1445
					'data' => [
1446
						'sprintf' => [
1447
							'format' => '<img src="%1$s" alt="%2$s" />',
1448
							'params' => [
1449
								'image_url' => false,
1450
								'filename' => true,
1451
							],
1452
						],
1453
						'class' => 'centertext',
1454
					],
1455
				],
1456
				'filename' => [
1457
					'header' => [
1458
						'value' => $txt['smileys_filename'],
1459
					],
1460
					'data' => [
1461
						'sprintf' => [
1462
							'format' => '%1$s.png',
1463
							'params' => [
1464
								'filename' => true,
1465
							],
1466
						],
1467
					],
1468
				],
1469
				'tooltip' => [
1470
					'header' => [
1471
						'value' => $txt['smileys_description'],
1472
					],
1473
					'data' => [
1474
						'db_htmlsafe' => 'title',
1475
					],
1476
				],
1477
				'board' => [
1478
					'header' => [
1479
						'value' => $txt['icons_board'],
1480
					],
1481
					'data' => [
1482
						'db' => 'board',
1483
					],
1484
				],
1485
				'modify' => [
1486
					'header' => [
1487
						'value' => $txt['smileys_modify'],
1488
					],
1489
					'data' => [
1490
						'sprintf' => [
1491
							'format' => '<a href="' . getUrl('admin', ['action' => 'admin', 'area' => 'smileys', 'sa' => 'editicon', 'icon' => '']) . '%1$s">' . $txt['smileys_modify'] . '</a>',
1492
							'params' => [
1493
								'id' => false,
1494
							],
1495
						],
1496
					],
1497
				],
1498
				'check' => [
1499
					'header' => [
1500
						'value' => '<input type="checkbox" onclick="invertAll(this, this.form);" class="input_check" />',
1501
						'class' => 'centertext',
1502
					],
1503
					'data' => [
1504
						'sprintf' => [
1505
							'format' => '<input type="checkbox" name="checked_icons[]" value="%1$d" class="input_check" />',
1506
							'params' => [
1507
								'id' => false,
1508
							],
1509
						],
1510
						'class' => 'centertext',
1511
					],
1512
				],
1513
			],
1514
			'form' => [
1515
				'href' => getUrl('admin', ['action' => 'admin', 'area' => 'smileys', 'sa' => 'editicons']),
1516
				'hidden_fields' => [
1517
					'icons_save' => 1,
1518
				]
1519
			],
1520
			'additional_rows' => [
1521
				[
1522
					'position' => 'below_table_data',
1523
					'class' => 'submitbutton',
1524
					'value' => '
1525
						<a class="linkbutton" href="' . getUrl('admin', ['action' => 'admin', 'area' => 'smileys', 'sa' => 'editicon']) . '">' . $txt['icons_add_new'] . '</a>
1526
						<input type="submit" name="delete" value="' . $txt['quickmod_delete_selected'] . '" onclick="return confirm(\'' . $txt['icons_confirm'] . '\');" />',
1527
				],
1528
				[
1529
					'position' => 'after_title',
1530
					'value' => $txt['icons_reorder_note'],
1531
				],
1532
			],
1533
			'javascript' => '
1534
				$().elkSortable({
1535
					sa: "messageiconorder",
1536
					error: "' . $txt['admin_order_error'] . '",
1537
					title: "' . $txt['admin_order_title'] . '",
1538
					placeholder: "ui-state-highlight",
1539
					href: "?action=admin;area=smileys;sa=editicons",
1540
					token: {token_var: "' . $token['admin-sort_token_var'] . '", token_id: "' . $token['admin-sort_token'] . '"}
1541
				});
1542
			',
1543
		];
1544
1545
		createList($listOptions);
1546
1547
		// If we're adding/editing an icon, we'll need a list of boards
1548
		if ($context['sub_action'] === 'editicon' || isset($this->_req->post->add))
1549
		{
1550
			// Force the sub_template just in case.
1551
			$context['sub_template'] = 'editicon';
1552
			$context['new_icon'] = !$this->_req->hasQuery('icon');
1553
1554
			// Get the properties of the current icon from the icon list.
1555
			if (!$context['new_icon'])
1556
			{
1557
				$iconId = $this->_req->getQuery('icon', 'intval', 0);
1558
				$context['icon'] = $context['icons'][$iconId] ?? [];
1559
			}
1560
1561
			// Get a list of boards needed for assigning this icon to a specific board.
1562
			$boardListOptions = [
1563
				'selected_board' => $context['icon']['board_id'] ?? 0,
1564
			];
1565
			require_once(SUBSDIR . '/Boards.subs.php');
1566
			$context += getBoardList($boardListOptions);
1567
		}
1568
	}
1569
1570
	/**
1571
	 * Callback function for createList().
1572
	 */
1573
	public function list_fetchMessageIconsDetails()
1574
	{
1575
		return fetchMessageIconsDetails();
1576
	}
1577
1578
	/**
1579
	 * Allows editing smileys order.
1580
	 */
1581
	public function action_setorder(): void
1582
	{
1583
		global $context, $txt;
1584
1585
		require_once(SUBSDIR . '/Smileys.subs.php');
1586
		$context['sub_template'] = 'setorder';
1587
1588
		// Move smileys to another position.
1589
		if ($this->_req->hasQuery('reorder'))
1590
		{
1591
			$locStr = $this->_req->getQuery('location', 'trim|strval', '');
1592
			$location = $locStr === 'popup' ? 2 : 0;
1593
			$source = $this->_req->getQuery('source', 'intval', 0);
1594
			$after = $this->_req->getQuery('after', 'intval', 0);
1595
			$row = $this->_req->getQuery('row', 'intval', 0);
1596
1597
			checkSession('get');
1598
1599
			if (empty($source))
1600
			{
1601
				throw new Exception('smiley_not_found', false);
1602
			}
1603
1604
			$smiley = [];
1605
1606
			if (!empty($after))
1607
			{
1608
				$smiley = getSmileyPosition($location, $after);
1609
				if (empty($smiley))
1610
				{
1611
					throw new Exception('smiley_not_found');
1612
				}
1613
			}
1614
			else
1615
			{
1616
				$smiley['row'] = $row;
1617
				$smiley['order'] = -1;
1618
				$smiley['location'] = $location;
1619
			}
1620
1621
			moveSmileyPosition($smiley, $source);
1622
		}
1623
1624
		$context['smileys'] = getSmileys();
1625
		$context['move_smiley'] = $this->_req->getQuery('move', 'intval', 0);
1626
1627
		// Make sure all rows are sequential.
1628
		foreach (array_keys($context['smileys']) as $location)
1629
		{
1630
			$context['smileys'][$location] = [
1631
				'id' => $location,
1632
				'title' => $location === 'postform' ? $txt['smileys_location_form'] : $txt['smileys_location_popup'],
1633
				'description' => $location === 'postform' ? $txt['smileys_location_form_description'] : $txt['smileys_location_popup_description'],
1634
				'last_row' => count($context['smileys'][$location]['rows']),
1635
				'rows' => array_values($context['smileys'][$location]['rows']),
1636
			];
1637
		}
1638
1639
		// Check & fix smileys that are not ordered properly in the database.
1640
		foreach (array_keys($context['smileys']) as $location)
1641
		{
1642
			foreach ($context['smileys'][$location]['rows'] as $id => $smiley_row)
1643
			{
1644
				// Fix empty rows if any.
1645
				if ($id != $smiley_row[0]['row'])
1646
				{
1647
					updateSmileyRow($id, $smiley_row[0]['row'], $location);
1648
1649
					// Only change the first row value of the first smiley (we don't need the others :P).
1650
					$context['smileys'][$location]['rows'][$id][0]['row'] = $id;
1651
				}
1652
1653
				// Make sure the smiley order is always sequential.
1654
				foreach ($smiley_row as $order_id => $smiley)
1655
				{
1656
					if ($order_id != $smiley['order'])
1657
					{
1658
						updateSmileyOrder($smiley['id'], $order_id);
1659
					}
1660
				}
1661
			}
1662
		}
1663
1664
		$this->clearSmileyCache();
1665
1666
		createToken('admin-sort');
1667
	}
1668
1669
	/**
1670
	 * Install a smiley set.
1671
	 */
1672
	public function action_install(): void
1673
	{
1674
		global $modSettings, $context, $txt, $scripturl;
1675
1676
		isAllowedTo('manage_smileys');
1677
		checkSession('request');
1678
1679
		// One of these two may be necessary
1680
		Txt::load('Errors');
1681
		Txt::load('Packages');
1682
1683
		$fileFunc = FileFunctions::instance();
1684
		require_once(SUBSDIR . '/Smileys.subs.php');
1685
		require_once(SUBSDIR . '/Package.subs.php');
1686
1687
		// Installing unless proven otherwise
1688
		$testing = false;
1689
		$destination = '';
1690
		$name = '';
1691
		$base_name = '';
1692
1693
		$setGz = $this->_req->getQuery('set_gz', 'trim|strval');
1694
		$package = $this->_req->getQuery('package', 'trim|strval');
1695
1696
		if ($setGz !== null)
1697
		{
1698
			$base_name = strtr(basename($setGz), ':/', '-_');
1699
			$name = Util::htmlspecialchars(strtok(basename($setGz), '.'));
1700
			$context['filename'] = $base_name;
1701
1702
			$destination = BOARDDIR . '/packages/' . $base_name;
1703
1704
			if ($fileFunc->fileExists($destination))
1705
			{
1706
				throw new Exception('package_upload_error_exists');
1707
			}
1708
1709
			// Let's copy it to the packages directory
1710
			file_put_contents($destination, fetch_web_data($setGz));
1711
			$testing = true;
1712
		}
1713
		elseif ($package !== null)
1714
		{
1715
			$base_name = basename($package);
1716
			$name = Util::htmlspecialchars(strtok(basename($package), '.'));
1717
			$context['filename'] = $base_name;
1718
1719
			$destination = BOARDDIR . '/packages/' . basename($package);
1720
		}
1721
1722
		if (!$fileFunc->fileExists($destination))
1723
		{
1724
			throw new Exception('package_no_file', false);
1725
		}
1726
1727
		// Make sure the temp directory exists and is empty.
1728
		if ($fileFunc->fileExists(BOARDDIR . '/packages/temp'))
1729
		{
1730
			deltree(BOARDDIR . '/packages/temp', false);
1731
		}
1732
1733
		if (!mktree(BOARDDIR . '/packages/temp'))
1734
		{
1735
			deltree(BOARDDIR . '/packages/temp', false);
1736
			$chmod_control = new PackageChmod();
1737
			$chmod_control->createChmodControl(
1738
				[BOARDDIR . '/packages/temp/delme.tmp'],
1739
				[
1740
					'destination_url' => $scripturl . '?action=admin;area=smileys;sa=install' . ($setGz !== null ? ';set_gz=' . $setGz : ''),
1741
					'crash_on_error' => true
1742
				]
1743
			);
1744
1745
			deltree(BOARDDIR . '/packages/temp', false);
1746
			throw new Exception('package_cant_download', false);
1747
		}
1748
1749
		$extracted = read_tgz_file($destination, BOARDDIR . '/packages/temp');
1750
1751
		// @todo needs to change the URL in the next line ;)
1752
		if (!$extracted)
1753
		{
1754
			throw new Exception('packageget_unable', false, ['https://custom.elkarte.net/index.php?action=search;type=12;basic_search=' . $name]);
1755
		}
1756
1757
		if (!$fileFunc->fileExists(BOARDDIR . '/packages/temp/package-info.xml'))
1758
		{
1759
			foreach ($extracted as $file)
1760
			{
1761
				if (basename($file['filename']) === 'package-info.xml')
1762
				{
1763
					$base_path = dirname($file['filename']) . '/';
1764
					break;
1765
				}
1766
			}
1767
		}
1768
1769
		if (!isset($base_path))
1770
		{
1771
			$base_path = '';
1772
		}
1773
1774
		if (!$fileFunc->fileExists(BOARDDIR . '/packages/temp/' . $base_path . 'package-info.xml'))
1775
		{
1776
			throw new Exception('package_get_error_missing_xml', false);
1777
		}
1778
1779
		$smileyInfo = getPackageInfo($context['filename']);
1780
		if (!is_array($smileyInfo))
1781
		{
1782
			throw new Exception($smileyInfo);
1783
		}
1784
1785
		// See if it is installed?
1786
		if (isSmileySetInstalled($smileyInfo['id']))
1787
		{
1788
			Errors::instance()->fatal_lang_error('package_installed_warning1');
1789
		}
1790
1791
		// Everything is fine, now it's time to do something; first we test
1792
		$parser = new PackageParser();
1793
		$actions = $parser->parsePackageInfo($smileyInfo['xml']);
1794
1795
		$context['post_url'] = getUrl('admin', ['action' => 'admin', 'area' => 'smileys', 'sa' => 'install', 'package' => $base_name]);
1796
		$context['has_failure'] = false;
1797
		$context['actions'] = [];
1798
		$context['ftp_needed'] = false;
1799
1800
		$bbc_parser = ParserWrapper::instance();
1801
1802
		foreach ($actions as $action)
1803
		{
1804
			if ($action['type'] === 'readme' || $action['type'] === 'license')
1805
			{
1806
				$type = 'package_' . $action['type'];
1807
				if ($fileFunc->fileExists(BOARDDIR . '/packages/temp/' . $base_path . $action['filename']))
1808
				{
1809
					$context[$type] = htmlspecialchars(trim(file_get_contents(BOARDDIR . '/packages/temp/' . $base_path . $action['filename']), "\n\r"), ENT_COMPAT, 'UTF-8');
1810
				}
1811
				elseif ($fileFunc->fileExists($action['filename']))
1812
				{
1813
					$context[$type] = htmlspecialchars(trim(file_get_contents($action['filename']), "\n\r"), ENT_COMPAT, 'UTF-8');
1814
				}
1815
1816
				if (!empty($action['parse_bbc']))
1817
				{
1818
					require_once(SUBSDIR . '/Post.subs.php');
1819
					preparsecode($context[$type]);
1820
					$context[$type] = $bbc_parser->parsePackage($context[$type]);
1821
				}
1822
				else
1823
				{
1824
					$context[$type] = nl2br($context[$type]);
1825
				}
1826
			}
1827
			elseif ($action['type'] === 'require-dir')
1828
			{
1829
				// Do this one...
1830
				$thisAction = [
1831
					'type' => $txt['package_extract'] . ' ' . $txt['package_tree'],
1832
					'action' => Util::htmlspecialchars(strtr($action['destination'], [BOARDDIR => '.']))
1833
				];
1834
1835
				$file = BOARDDIR . '/packages/temp/' . $base_path . $action['filename'];
1836
				if (isset($action['filename']) && (!$fileFunc->fileExists($file) || !$fileFunc->isWritable(dirname($action['destination']))))
1837
				{
1838
					$context['has_failure'] = true;
1839
1840
					$thisAction += [
1841
						'description' => $txt['package_action_error'],
1842
						'failed' => true,
1843
					];
1844
				}
1845
1846
				// Show a description for the action if one is provided
1847
				if (empty($thisAction['description']))
1848
				{
1849
					$thisAction['description'] = $action['description'] ?? '';
1850
				}
1851
1852
				$context['actions'][] = $thisAction;
1853
			}
1854
			elseif ($action['type'] === 'credits')
1855
			{
1856
				// Time to build the billboard
1857
				$credits_tag = [
1858
					'url' => $action['url'],
1859
					'license' => $action['license'],
1860
					'copyright' => $action['copyright'],
1861
					'title' => $action['title'],
1862
				];
1863
			}
1864
		}
1865
1866
		if ($testing)
1867
		{
1868
			$context['sub_template'] = 'view_package';
1869
			$context['uninstalling'] = false;
1870
			$context['is_installed'] = false;
1871
			$context['package_name'] = $smileyInfo['name'];
1872
			theme()->getTemplates()->load('Packages');
1873
		}
1874
		// Do the actual installation
1875
		else
1876
		{
1877
			foreach ($context['actions'] as $action)
1878
			{
1879
				updateSettings([
1880
					'smiley_sets_known' => $modSettings['smiley_sets_known'] . ',' . basename($action['action']),
1881
					'smiley_sets_names' => $modSettings['smiley_sets_names'] . "\n" . $smileyInfo['name'] . (count($context['actions']) > 1 ? ' ' . (empty($action['description']) ? basename($action['action']) : Util::htmlspecialchars($action['description'])) : ''),
1882
				]);
1883
			}
1884
1885
			package_flush_cache();
1886
1887
			// Time to tell pacman we have a new package installed!
1888
			package_put_contents(BOARDDIR . '/packages/installed.list', time());
1889
1890
			// Credits tag?
1891
			$credits_tag = (empty($credits_tag)) ? '' : serialize($credits_tag);
1892
			$installed = [
1893
				'filename' => $smileyInfo['filename'],
1894
				'name' => $smileyInfo['name'],
1895
				'package_id' => $smileyInfo['id'],
1896
				'version' => $smileyInfo['filename'],
1897
				'id_member' => $this->user->id,
1898
				'member_name' => $this->user->name,
1899
				'credits_tag' => $credits_tag,
1900
			];
1901
			logPackageInstall($installed);
1902
1903
			logAction('install_package', ['package' => Util::htmlspecialchars($smileyInfo['name']), 'version' => Util::htmlspecialchars($smileyInfo['version'])], 'admin');
1904
1905
			$this->clearSmileyCache();
1906
		}
1907
1908
		if ($fileFunc->fileExists(BOARDDIR . '/packages/temp'))
1909
		{
1910
			deltree(BOARDDIR . '/packages/temp');
1911
		}
1912
1913
		if (!$testing)
1914
		{
1915
			redirectexit('action=admin;area=smileys');
1916
		}
1917
	}
1918
1919
	/**
1920
	 * Load known smiley set information into context
1921
	 *
1922
	 * @return void
1923
	 */
1924
	public function loadSmileySets(): void
1925
	{
1926
		global $context, $modSettings;
1927
1928
		$set_paths = explode(',', $modSettings['smiley_sets_known']);
1929
		$set_names = explode("\n", $modSettings['smiley_sets_names']);
1930
		$set_exts = isset($modSettings['smiley_sets_extensions'])
1931
			? explode(',', $modSettings['smiley_sets_extensions'])
1932
			: setSmileyExtensionArray();
1933
1934
		foreach ($set_paths as $i => $set)
1935
		{
1936
			$context['smiley_sets'][$i] = [
1937
				'id' => $i,
1938
				'path' => htmlspecialchars($set, ENT_COMPAT),
1939
				'name' => htmlspecialchars(stripslashes($set_names[$i]), ENT_COMPAT),
1940
				'selected' => $set === $modSettings['smiley_sets_default'],
1941
				'ext' => Util::htmlspecialchars($set_exts[$i]),
1942
			];
1943
		}
1944
	}
1945
1946
	/**
1947
	 * Perhaps a longer name for the function would better describe what this does. So it will
1948
	 * search group of directories and return just the unique filenames, dis-regarding the extension.
1949
	 * This allows us to match by name across sets that have different extensions
1950
	 *
1951
	 * @param array $smiley_sets array of smiley sets (end directory names) to search
1952
	 * @return array of unique smiley names across one or many "sets"
1953
	 */
1954
	public function getAllPossibleFilenamesForTheSmileys(array $smiley_sets): array
1955
	{
1956
		global $context, $modSettings;
1957
1958
		$filenames = [];
1959
		$fileFunc = FileFunctions::instance();
1960
1961
		$smileys_dir = empty($modSettings['smileys_dir']) ? BOARDDIR . '/smileys' : $modSettings['smileys_dir'];
1962
		if (!$fileFunc->isDir($smileys_dir))
1963
		{
1964
			return [];
1965
		}
1966
1967
		foreach ($smiley_sets as $smiley_set)
1968
		{
1969
			$smiles = $fileFunc->listTree($context['smileys_dir'] . '/' . un_htmlspecialchars($smiley_set['path']));
1970
			foreach ($smiles as $smile)
1971
			{
1972
				if (in_array($smile['filename'], ['.', '..', '.htaccess', 'index.php'], true))
1973
				{
1974
					continue;
1975
				}
1976
1977
				$key = strtolower(pathinfo($smile['filename'], PATHINFO_FILENAME));
0 ignored issues
show
Bug introduced by
It seems like pathinfo($smile['filenam...ller\PATHINFO_FILENAME) can also be of type array; however, parameter $string of strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1977
				$key = strtolower(/** @scrutinizer ignore-type */ pathinfo($smile['filename'], PATHINFO_FILENAME));
Loading history...
1978
				if (in_array($key, $filenames, true))
1979
				{
1980
					continue;
1981
				}
1982
1983
				if (!in_array(strtolower(pathinfo($smile['filename'], PATHINFO_EXTENSION)), $this->_smiley_types, true))
1984
				{
1985
					continue;
1986
				}
1987
1988
				$filenames[strtolower($key)] = [
1989
					'id' => Util::htmlspecialchars($key),
1990
					'selected' => false,
1991
				];
1992
			}
1993
		}
1994
1995
		ksort($filenames);
1996
1997
		return $filenames;
1998
	}
1999
}
2000