ManageSmileys   F
last analyzed

Complexity

Total Complexity 223

Size/Duplication

Total Lines 1944
Duplicated Lines 0 %

Test Coverage

Coverage 2.05%

Importance

Changes 0
Metric Value
eloc 907
dl 0
loc 1944
ccs 15
cts 733
cp 0.0205
rs 1.693
c 0
b 0
f 0
wmc 223

21 Methods

Rating   Name   Duplication   Size   Complexity  
A _subActionImport() 0 21 3
A action_index() 0 55 2
A settings_search() 0 3 1
F action_install() 0 241 34
A list_fetchMessageIconsDetails() 0 3 1
A _initSmileyContext() 0 11 2
F action_editsmiley() 0 327 30
B getAllPossibleFilenamesForTheSmileys() 0 44 8
A loadSmileySets() 0 18 3
A _settings() 0 25 2
A clearSmileyCache() 0 6 1
A action_smileySettings_display() 0 35 2
D _subActionSubmit() 0 100 20
B _getUniqueSmileys() 0 36 7
F action_addsmiley() 0 240 38
C _subActionModifySet() 0 63 13
B action_edit() 0 133 3
F action_editicon() 0 261 27
A importSmileys() 0 34 6
F action_setorder() 0 85 15
A createCustomTagsFile() 0 44 5

How to fix   Complexity   

Complex Class

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

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

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

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

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

1959
				$key = strtolower(/** @scrutinizer ignore-type */ pathinfo($smile['filename'], PATHINFO_FILENAME));
Loading history...
1960
				if (in_array($key, $filenames, true))
1961
				{
1962
					continue;
1963
				}
1964
1965
				if (!in_array(strtolower(pathinfo($smile['filename'], PATHINFO_EXTENSION)), $this->_smiley_types, true))
1966
				{
1967
					continue;
1968
				}
1969
1970
				$filenames[strtolower($key)] = [
1971
					'id' => Util::htmlspecialchars($key, ENT_COMPAT, 'UTF-8'),
1972
					'selected' => false,
1973
				];
1974
			}
1975
		}
1976
1977
		ksort($filenames);
1978
1979
		return $filenames;
1980
	}
1981
}
1982