ManageSmileys   F
last analyzed

Complexity

Total Complexity 223

Size/Duplication

Total Lines 1946
Duplicated Lines 0 %

Test Coverage

Coverage 2.05%

Importance

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

21 Methods

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

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