Packages::action_install2()   F
last analyzed

Complexity

Conditions 28
Paths 5094

Size

Total Lines 214
Code Lines 92

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 28
eloc 92
c 1
b 0
f 0
nc 5094
nop 0
dl 0
loc 214
rs 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file is the main Package Manager.
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 Beta 1
14
 *
15
 */
16
17
namespace ElkArte\Packages;
18
19
use BBC\ParserWrapper;
20
use ElkArte\AbstractController;
21
use ElkArte\Action;
22
use ElkArte\Cache\Cache;
23
use ElkArte\Database\AbstractTable;
24
use ElkArte\EventManager;
25
use ElkArte\Exceptions\Exception;
26
use ElkArte\Helper\FileFunctions;
27
use ElkArte\Helper\Util;
28
use ElkArte\Languages\Txt;
29
use ElkArte\User;
30
use FilesystemIterator;
31
use UnexpectedValueException;
32
33
/**
34
 * This class is the administration package manager controller.
35
 * Its main job is to install/uninstall, allow to browse, packages.
36
 * In fact, just about everything is related to addon packages, including FTP connections when necessary.
37
 *
38
 * @package Packages
39
 */
40
class Packages extends AbstractController
41
{
42
	/** @var array|bool listing of files in a package */
43
	private $_extracted_files;
44
45
	/** @var int The id from the DB or an installed package */
46
	public $install_id;
47
48
	/** @var string[] Array of installed theme paths */
49
	public $theme_paths;
50
51
	/** @var array Array of files / directories that require permissions */
52
	public $chmod_files;
53
54
	/** @var string Filename of the package */
55
	private $_filename;
56
57
	/** @var string Base path of the package */
58
	private $_base_path;
59
60
	/** @var bool If this is an uninstallation pass or not */
61
	private $_uninstalling;
62
63
	/** @var bool If the package is installed, previously or not */
64
	private $_is_installed;
65
66
	/** @var FileFunctions */
67
	private $fileFunc;
68
69
	/**
70
	 * Pre-dispatch, called before other methods.
71
	 */
72
	public function pre_dispatch()
73
	{
74
		// Generic subs for this controller
75
		require_once(SUBSDIR . '/Package.subs.php');
76
77
		// Load all the basic stuff.
78
		Txt::load('Packages');
79
		theme()->getTemplates()->load('Packages');
80
		loadCSSFile('admin.css');
81
82
		$this->fileFunc = FileFunctions::instance();
83
	}
84
85
	/**
86
	 * Entry point, the default method of this controller.
87
	 *
88
	 * @event integrate_sa_packages
89
	 * @see AbstractController::action_index
90
	 */
91
	public function action_index()
92
	{
93
		global $txt, $context;
94
95
		// Admins-only!
96
		isAllowedTo('admin_forum');
97
98
		$context['page_title'] = $txt['package'];
99
100
		// Delegation makes the world... that is, the package manager go 'round.
101
		$subActions = [
102
			'browse' => [$this, 'action_browse'],
103
			'remove' => [$this, 'action_remove'],
104
			'list' => [$this, 'action_list'],
105
			'ftptest' => [$this, 'action_ftptest'],
106
			'install' => [$this, 'action_install'],
107
			'install2' => [$this, 'action_install2'],
108
			'uninstall' => [$this, 'action_install'],
109
			'uninstall2' => [$this, 'action_install2'],
110
			'options' => [$this, 'action_options'],
111
			'flush' => [$this, 'action_flush'],
112
			'examine' => [$this, 'action_examine'],
113
			'showoperations' => [$this, 'action_showoperations'],
114
			'servers' => ['controller' => PackageServers::class, 'function' => 'action_list'],
115
			'upload' => ['controller' => PackageServers::class,	'function' => 'action_upload'],
116
		];
117
118
		// Set up action/subaction stuff.
119
		$action = new Action('packages');
120
121
		// Set up some tabs...
122
		$context[$context['admin_menu_name']]['object']->prepareTabData([
123
			'title' => 'package_manager',
124
			'description' => 'package_manager_desc',
125
			'class' => 'i-package',
126
		]);
127
128
		// Work out exactly who it is we are calling. call integrate_sa_packages
129
		$subAction = $action->initialize($subActions, 'browse');
130
131
		// Set up for the template
132
		$context['sub_action'] = $subAction;
133
134
		// Let's just do it!
135
		$action->dispatch($subAction);
136
	}
137
138
	/**
139
	 * Test install/uninstall a package.
140
	 */
141
	public function action_install(): void
142
	{
143
		global $txt, $context;
144
145
		// You have to specify a file!!
146
		$file = $this->_req->getQuery('package', 'trim');
147
		if (empty($file))
148
		{
149
			redirectexit('action=admin;area=packages');
150
		}
151
152
		// What are we trying to do?
153
		$this->_filename = (string) preg_replace('~[.]+~', '.', $file);
154
		$this->_uninstalling = $this->_req->query->sa === 'uninstall';
155
156
		// If we can't find the file, our installation ends here
157
		if (!$this->fileFunc->fileExists(BOARDDIR . '/packages/' . $this->_filename))
158
		{
159
			throw new Exception('package_no_file', false);
160
		}
161
162
		// Do we have an existing id, for uninstallations and the like?
163
		$this->install_id = $this->_req->getQuery('pid', 'intval', 0);
164
165
		// This will be needed
166
		require_once(SUBSDIR . '/Themes.subs.php');
167
168
		// Load up the package FTP information?
169
		$create_chmod_control = new PackageChmod();
170
		$create_chmod_control->createChmodControl();
171
172
		// Make sure our temp directory exists and is empty.
173
		if ($this->fileFunc->isDir(BOARDDIR . '/packages/temp'))
174
		{
175
			deltree(BOARDDIR . '/packages/temp', false);
176
		}
177
		else
178
		{
179
			$this->_create_temp_dir();
180
		}
181
182
		// Extract the files in to the temp, so we can get things like the readme, etc.
183
		$this->_extract_files_temp();
184
185
		// Load up any custom themes we may want to install into...
186
		$this->theme_paths = getThemesPathbyID();
187
188
		// Get the package info...
189
		$packageInfo = getPackageInfo($this->_filename);
190
		if (!is_array($packageInfo))
191
		{
192
			throw new Exception($packageInfo);
193
		}
194
195
		$packageInfo['filename'] = $this->_filename;
196
197
		// The addon isn't installed... unless proven otherwise.
198
		$this->_is_installed = false;
199
200
		// See if it is installed?
201
		$package_installed = isPackageInstalled($packageInfo['id'], $this->install_id);
202
203
		// Any database actions
204
		$this->determineDatabaseChanges($packageInfo, $package_installed);
205
206
		$actions = $this->_get_package_actions($package_installed, $packageInfo);
207
		$context['actions'] = [];
208
		$context['ftp_needed'] = false;
209
210
		// No actions found, return so we can display an error
211
		if (empty($actions))
212
		{
213
			redirectexit('action=admin;area=packages');
214
		}
215
216
		// Now prepare things for the template using the package actions class
217
		$pka = new PackageActions(new EventManager());
218
		$pka->setUser(User::$info);
219
		$pka->test_init($actions, $this->_uninstalling, $this->_base_path, $this->theme_paths);
220
221
		$context['has_failure'] = $pka->has_failure;
222
		$context['failure_details'] = $pka->failure_details;
223
		$context['actions'] = $pka->ourActions;
224
225
		// Change our last link tree item for more information on this Packages area.
226
		$context['breadcrumbs'][count($context['breadcrumbs']) - 1] = [
227
			'url' => getUrl('admin', ['action' => 'admin', 'area' => 'packages', 'sa' => 'browse']),
228
			'name' => $this->_uninstalling ? $txt['package_uninstall_actions'] : $txt['install_actions']
229
		];
230
231
		// All things to make the template go round
232
		$context['page_title'] .= ' - ' . ($this->_uninstalling ? $txt['package_uninstall_actions'] : $txt['install_actions']);
233
		$context['sub_template'] = 'view_package';
234
		$context['filename'] = $this->_filename;
235
		$context['package_name'] = $packageInfo['name'] ?? $this->_filename;
236
		$context['is_installed'] = $this->_is_installed;
237
		$context['uninstalling'] = $this->_uninstalling;
238
		$context['extract_type'] = $packageInfo['type'] ?? 'modification';
239
240
		// Have we got some things that we might want to do "multi-theme"?
241
		$this->_multi_theme($pka->themeFinds['candidates']);
242
243
		// Trash the cache... which will also check permissions for us!
244
		package_flush_cache(true);
245
246
		// Clear the temp directory
247
		if ($this->fileFunc->isDir(BOARDDIR . '/packages/temp'))
248
		{
249
			deltree(BOARDDIR . '/packages/temp');
250
		}
251
252
		// Will we require chmod permissions to pull this off?
253
		$this->chmod_files = empty($pka->chmod_files) ? [] : $pka->chmod_files;
254
		if (!empty($this->chmod_files))
255
		{
256
			$chmod_control = new PackageChmod();
257
			$ftp_status = $chmod_control->createChmodControl($this->chmod_files);
258
			$context['ftp_needed'] = !empty($ftp_status['files']['notwritable']) && !empty($context['package_ftp']);
259
		}
260
261
		$context['post_url'] = getUrl('admin', ['action' => 'admin', 'area' => 'packages', 'sa' => ($this->_uninstalling ? 'uninstall' : 'install') . ($context['ftp_needed'] ? '' : '2'), 'package' => $this->_filename, 'pid' => $this->install_id]);
262
		checkSubmitOnce('register');
263
	}
264
265
	/**
266
	 * Creates the packages temp directory
267
	 *
268
	 * - First try as 755, failing moves to 777
269
	 * - Will try with FTP permissions for cases where the web server credentials
270
	 * do not have "create" directory permissions
271
	 *
272
	 * @throws Exception when no directory can be made
273
	 */
274
	private function _create_temp_dir(): void
275
	{
276
		global $context, $scripturl, $package_ftp;
277
278
		// Try to Make the temp directory
279
		if (!mktree(BOARDDIR . '/packages/temp'))
280
		{
281
			deltree(BOARDDIR . '/packages/temp', false);
282
			$chmod_control = new PackageChmod();
283
			$chmod_control->createChmodControl(
284
				[BOARDDIR . '/packages/temp/delme.tmp'],
285
				[
286
					'destination_url' => $scripturl . '?action=admin;area=packages;sa=' . $this->_req->query->sa . ';package=' . ($context['filename'] ?? ''),
287
					'crash_on_error' => true
288
				]
289
			);
290
291
			// No temp directory was able to be made, that's fatal
292
			deltree(BOARDDIR . '/packages/temp', false);
293
			unset($package_ftp, $_SESSION['ftp_connection']);
294
			throw new Exception('package_cant_download', false);
295
		}
296
	}
297
298
	/**
299
	 * Extracts a package file in the packages/temp directory
300
	 *
301
	 * - Sets the base path as needed
302
	 * - Loads $extracted_files with the package file listing
303
	 */
304
	private function _extract_files_temp(): void
305
	{
306
		// Is it a file in the package directory?
307
		if (is_file(BOARDDIR . '/packages/' . $this->_filename))
308
		{
309
			// Unpack the files in to the packages/temp directory
310
			$this->_extracted_files = read_tgz_file(BOARDDIR . '/packages/' . $this->_filename, BOARDDIR . '/packages/temp');
311
312
			// Determine the base path for the package
313
			if ($this->_extracted_files && !$this->fileFunc->fileExists(BOARDDIR . '/packages/temp/package-info.xml'))
314
			{
315
				foreach ($this->_extracted_files as $file)
316
				{
317
					if (basename($file['filename']) === 'package-info.xml')
318
					{
319
						$this->_base_path = dirname($file['filename']) . '/';
320
						break;
321
					}
322
				}
323
			}
324
325
			if ($this->_base_path === null)
326
			{
327
				$this->_base_path = '';
328
			}
329
		}
330
		// Perhaps it's a directory then, assumed to be extracted
331
		elseif (!empty($this->_filename) && $this->fileFunc->isDir(BOARDDIR . '/packages/' . $this->_filename))
332
		{
333
			// Copy the directory to the temp directory
334
			copytree(BOARDDIR . '/packages/' . $this->_filename, BOARDDIR . '/packages/temp');
335
336
			// Get the file listing
337
			$this->_extracted_files = $this->fileFunc->listtree(BOARDDIR . '/packages/temp');
338
			$this->_base_path = '';
339
		}
340
		// Well, we don't know what it is then, so we stop
341
		else
342
		{
343
			throw new Exception('no_access', false);
344
		}
345
	}
346
347
	/**
348
	 * Returns the actions that are required to install / uninstall / upgrade a package.
349
	 * Actions are defined by parsePackageInfo
350
	 * Sets the is_installed flag
351
	 *
352
	 * @param array $package_installed
353
	 * @param array $packageInfo Details for the package being tested/installed, set by getPackageInfo
354
	 * @param bool $testing passed to parsePackageInfo, true for test install, false for real installation
355
	 *
356
	 * @return array
357
	 * @throws Exception package_cant_uninstall, package_uninstall_cannot
358
	 */
359
	private function _get_package_actions($package_installed, $packageInfo, $testing = true): array
360
	{
361
		global $context;
362
363
		$actions = [];
364
365
		// Uninstalling?
366
		if ($this->_uninstalling)
367
		{
368
			// Wait, it's not installed yet!
369
			if (!isset($package_installed['old_version']))
370
			{
371
				deltree(BOARDDIR . '/packages/temp');
372
				throw new Exception('package_cant_uninstall', false);
373
			}
374
375
			$parser = new PackageParser();
376
			$actions = $parser->parsePackageInfo($packageInfo['xml'], $testing, 'uninstall');
377
378
			// Gadzooks!  There's no uninstaller at all!?
379
			if (empty($actions))
380
			{
381
				deltree(BOARDDIR . '/packages/temp');
382
				throw new Exception('package_uninstall_cannot', false);
383
			}
384
385
			// Can't edit the custom themes it's edited if you're uninstalling, they must be removed.
386
			$context['themes_locked'] = true;
387
388
			// Only let them uninstall themes it was installed into.
389
			foreach (array_keys($this->theme_paths) as $id)
390
			{
391
				if ($id === 1)
392
				{
393
					continue;
394
				}
395
396
				if (in_array($id, $package_installed['old_themes']))
397
				{
398
					continue;
399
				}
400
401
				unset($this->theme_paths[$id]);
402
			}
403
		}
404
		// Or is it already installed and you want to upgrade?
405
		elseif (isset($package_installed['old_version']) && $package_installed['old_version'] != $packageInfo['version'])
406
		{
407
			// Look for an upgrade...
408
			$parser = new PackageParser();
409
			$actions = $parser->parsePackageInfo($packageInfo['xml'], $testing, 'upgrade', $package_installed['old_version']);
410
411
			// There was no upgrade...
412
			if (empty($actions))
413
			{
414
				$this->_is_installed = true;
415
			}
416
			else
417
			{
418
				// Otherwise they can only upgrade themes from the first time around.
419
				foreach (array_keys($this->theme_paths) as $id)
420
				{
421
					if ($id === 1)
422
					{
423
						continue;
424
					}
425
426
					if (in_array($id, $package_installed['old_themes']))
427
					{
428
						continue;
429
					}
430
431
					unset($this->theme_paths[$id]);
432
				}
433
			}
434
		}
435
		// Simply already installed
436
		elseif (isset($package_installed['old_version']) && $package_installed['old_version'] == $packageInfo['version'])
437
		{
438
			$this->_is_installed = true;
439
		}
440
441
		if (!isset($package_installed['old_version']) || $this->_is_installed)
442
		{
443
			$parser = new PackageParser();
444
			$actions = $parser->parsePackageInfo($packageInfo['xml'], $testing);
445
		}
446
447
		return $actions;
448
	}
449
450
	/**
451
	 * Determines the availability / validity of installing a package in any of the installed themes
452
	 *
453
	 * @param array $themeFinds
454
	 */
455
	private function _multi_theme($themeFinds): void
456
	{
457
		global $settings, $txt, $context;
458
459
		if (!empty($themeFinds['candidates']))
460
		{
461
			foreach ($themeFinds['candidates'] as $action_data)
462
			{
463
				// Get the part of the file we'll be dealing with.
464
				preg_match('~^\$(languagedir|languages_dir|imagesdir|themedir)(\\|/)*(.+)*~i', $action_data['unparsed_destination'], $matches);
465
				$path = '';
466
				if ($matches[1] === 'imagesdir')
467
				{
468
					$path = '/' . basename($settings['default_images_url']);
469
				}
470
				elseif ($matches[1] === 'languagedir' || $matches[1] === 'languages_dir')
471
				{
472
					$path = '/ElkArte/Languages';
473
				}
474
475
				if (!empty($matches[3]))
476
				{
477
					$path .= $matches[3];
478
				}
479
480
				if (!$this->_uninstalling)
481
				{
482
					$path .= '/' . basename($action_data['filename']);
483
				}
484
485
				// Loop through each custom theme to note its candidacy!
486
				foreach ($this->theme_paths as $id => $theme_data)
487
				{
488
					$id = (int) $id;
489
					if (isset($theme_data['theme_dir']) && $id !== 1)
490
					{
491
						$real_path = $theme_data['theme_dir'] . $path;
492
493
						// Confirm that we don't already have this dealt with by another entry.
494
						if (!in_array(strtolower(strtr($real_path, ['\\' => '/'])), $themeFinds['other_themes'], true))
495
						{
496
							// Check if we will need to chmod this.
497
							if (!dirTest(dirname($real_path)))
498
							{
499
								$temp = dirname($real_path);
500
								while (!$this->fileFunc->fileExists($temp) && strlen($temp) > 1)
501
								{
502
									$temp = dirname($temp);
503
								}
504
505
								$this->chmod_files[] = $temp;
506
							}
507
508
							if ($action_data['type'] === 'require-dir'
509
								&& !$this->fileFunc->isWritable($real_path)
510
								&& ($this->fileFunc->fileExists($real_path) || !$this->fileFunc->isWritable(dirname($real_path))))
511
							{
512
								$this->chmod_files[] = $real_path;
513
							}
514
515
							if (!isset($context['theme_actions'][$id]))
516
							{
517
								$context['theme_actions'][$id] = [
518
									'name' => $theme_data['name'],
519
									'actions' => [],
520
								];
521
							}
522
523
							if ($this->_uninstalling)
524
							{
525
								$context['theme_actions'][$id]['actions'][] = [
526
									'type' => $txt['package_delete'] . ' ' . ($action_data['type'] === 'require-dir' ? $txt['package_tree'] : $txt['package_file']),
527
									'action' => strtr($real_path, ['\\' => '/', BOARDDIR => '.']),
528
									'description' => '',
529
									'value' => base64_encode(json_encode(['type' => $action_data['type'], 'orig' => $action_data['filename'], 'future' => $real_path, 'id' => $id])),
530
									'not_mod' => true,
531
								];
532
							}
533
							else
534
							{
535
								$context['theme_actions'][$id]['actions'][] = [
536
									'type' => $txt['package_extract'] . ' ' . ($action_data['type'] === 'require-dir' ? $txt['package_tree'] : $txt['package_file']),
537
									'action' => strtr($real_path, ['\\' => '/', BOARDDIR => '.']),
538
									'description' => '',
539
									'value' => base64_encode(json_encode(['type' => $action_data['type'], 'orig' => $action_data['destination'], 'future' => $real_path, 'id' => $id])),
540
									'not_mod' => true,
541
								];
542
							}
543
						}
544
					}
545
				}
546
			}
547
		}
548
	}
549
550
	/**
551
	 * Actually installs/uninstalls a package
552
	 */
553
	public function action_install2(): void
554
	{
555
		global $txt, $context, $modSettings;
556
557
		// Make sure we don't install this addon twice.
558
		checkSubmitOnce('check');
559
		checkSession();
560
561
		// If there's no package file, what are we installing?
562
		$this->_filename = $this->_req->getQuery('package', 'trim');
563
		if (empty($this->_filename))
564
		{
565
			redirectexit('action=admin;area=packages');
566
		}
567
568
		// And if the file does not exist, there is a problem
569
		if (!$this->fileFunc->fileExists(BOARDDIR . '/packages/' . $this->_filename))
570
		{
571
			throw new Exception('package_no_file', false);
572
		}
573
574
		// If this is an uninstallation, we'll have an id.
575
		$this->install_id = $this->_req->getQuery('pid', 'intval', 0);
576
577
		// Installing in themes will require some help
578
		require_once(SUBSDIR . '/Themes.subs.php');
579
580
		$this->_uninstalling = $this->_req->query->sa === 'uninstall2';
581
582
		// Load up the package FTP information?
583
		$chmod_control = new PackageChmod();
584
		$chmod_control->createChmodControl(
585
			[],
586
			[
587
				'destination_url' => getUrl('admin', ['action' => 'admin', 'area' => 'packages', 'sa' => $this->_req->query->sa, 'package' => $this->_req->query->package])
588
			]
589
		);
590
591
		// Make sure the temp directory exists and is empty!
592
		if ($this->fileFunc->isDir(BOARDDIR . '/packages/temp'))
593
		{
594
			deltree(BOARDDIR . '/packages/temp', false);
595
		}
596
		else
597
		{
598
			$this->_create_temp_dir();
599
		}
600
601
		// Let the unpacker do the work.
602
		$this->_extract_files_temp();
603
604
		// Are we installing this into any custom themes?
605
		$custom_themes = $this->_getCustomThemes();
606
607
		// Now load up the paths of the themes that we need to know about.
608
		$this->theme_paths = getThemesPathbyID($custom_themes);
609
610
		// Is there any theme copying that we want to take place?
611
		$themes_installed = $this->_installThemes();
612
613
		// Get the package info...
614
		$packageInfo = getPackageInfo($this->_filename);
615
		if (!is_array($packageInfo))
616
		{
617
			throw new Exception($packageInfo);
618
		}
619
620
		$packageInfo['filename'] = $this->_filename;
621
622
		$context['base_path'] = $this->_base_path;
623
		$context['extracted_files'] = $this->_extracted_files;
624
625
		// Create a backup file to roll back to! (but if they do this more than once, don't run it a zillion times.)
626
		if (!empty($modSettings['package_make_full_backups']) && (!isset($_SESSION['last_backup_for']) || $_SESSION['last_backup_for'] != $this->_filename . ($this->_uninstalling ? '$$' : '$')))
627
		{
628
			$_SESSION['last_backup_for'] = $this->_filename . ($this->_uninstalling ? '$$' : '$');
629
630
			package_create_backup(($this->_uninstalling ? 'backup_' : 'before_') . strtok($this->_filename, '.'));
631
		}
632
633
		// The addon isn't installed... unless proven otherwise.
634
		$this->_is_installed = false;
635
636
		// Is it actually installed?
637
		$package_installed = isPackageInstalled($packageInfo['id'], $this->install_id);
638
639
		// Fetch the installation status and action log
640
		$install_log = $this->_get_package_actions($package_installed, $packageInfo, false);
641
642
		// Set up the details for the sub template, breadcrumbs, etc.
643
		$context['breadcrumbs'][count($context['breadcrumbs']) - 1] = [
644
			'url' => getUrl('admin', ['action' => 'admin', 'area' => 'packages', 'sa' => 'browse']),
645
			'name' => $this->_uninstalling ? $txt['uninstall'] : $txt['extracting']
646
		];
647
		$context['page_title'] .= ' - ' . ($this->_uninstalling ? $txt['uninstall'] : $txt['extracting']);
648
		$context['sub_template'] = 'extract_package';
649
		$context['filename'] = $this->_filename;
650
		$context['install_finished'] = false;
651
		$context['is_installed'] = $this->_is_installed;
652
		$context['uninstalling'] = $this->_uninstalling;
653
		$context['extract_type'] = $packageInfo['type'] ?? 'modification';
654
655
		// We're gonna be needing the table db functions! ...Sometimes.
656
		$table_installer = db_table();
657
658
		// @todo Make a log of any errors that occurred and output them?
659
		if (!empty($install_log))
660
		{
661
			$pka = new PackageActions(new EventManager());
662
			$pka->setUser(User::$info);
663
			$pka->install_init($install_log, $this->_uninstalling, $this->_base_path, $this->theme_paths, $themes_installed);
664
			$failed_steps = $pka->failed_steps;
665
			$themes_installed = $pka->themes_installed;
666
667
			package_flush_cache();
668
669
			// First, ensure this change doesn't get removed by putting a stake in the ground (So to speak).
670
			package_put_contents(BOARDDIR . '/packages/installed.list', time());
671
672
			// See if this is already installed
673
			$is_upgrade = false;
674
			$old_db_changes = [];
675
			$package_check = isPackageInstalled($packageInfo['id']);
676
677
			// Change the installed state as required.
678
			if (!empty($package_check['install_state']))
679
			{
680
				if ($this->_uninstalling)
681
				{
682
					setPackageState($package_check['package_id'], $this->install_id);
683
				}
684
				else
685
				{
686
					// not uninstalling so must be an upgrade
687
					$is_upgrade = true;
688
					$old_db_changes = empty($package_check['db_changes']) ? [] : $package_check['db_changes'];
689
				}
690
			}
691
692
			// Assuming we're not uninstalling, add the entry.
693
			if (!$this->_uninstalling)
694
			{
695
				// Any db changes from an older version?
696
				$table_log = $table_installer->package_log();
697
698
				if (!empty($old_db_changes))
699
				{
700
					$db_package_log = empty($table_log) ? $old_db_changes : array_merge($old_db_changes, $table_log);
701
				}
702
				else
703
				{
704
					$db_package_log = $table_log;
705
				}
706
707
				// If there are some database changes we might want to remove then filter them out.
708
				if (!empty($db_package_log))
709
				{
710
					// We're really just checking for entries which are creating table AND add columns (etc.).
711
					$tables = [];
712
					usort($db_package_log, fn(array $a, array $b): int => $this->_sort_table_first($a, $b));
713
					foreach ($db_package_log as $k => $log)
714
					{
715
						if ($log[0] === 'remove_table')
716
						{
717
							$tables[] = $log[1];
718
						}
719
						elseif (in_array($log[1], $tables, true))
720
						{
721
							unset($db_package_log[$k]);
722
						}
723
					}
724
725
					$package_installed['db_changes'] = serialize($db_package_log);
726
				}
727
				else
728
				{
729
					$package_installed['db_changes'] = '';
730
				}
731
732
				// What themes did we actually install?
733
				$themes_installed = array_unique($themes_installed);
734
				$themes_installed = implode(',', $themes_installed);
735
736
				// What failed steps?
737
				$failed_step_insert = serialize($failed_steps);
738
739
				// Credits tag?
740
				$credits_tag = (empty($pka->credits_tag)) ? '' : serialize($pka->credits_tag);
741
742
				// Add to the log packages
743
				addPackageLog($packageInfo, $failed_step_insert, $themes_installed, $package_installed['db_changes'], $is_upgrade, $credits_tag);
744
			}
745
746
			$context['install_finished'] = true;
747
		}
748
749
		// If there are database changes - and they want them removed - let's do it last!
750
		$this->removeDatabaseChanges($package_installed, $table_installer);
751
752
		// Clean house... get rid of the evidence ;).
753
		if ($this->fileFunc->isDir(BOARDDIR . '/packages/temp'))
754
		{
755
			deltree(BOARDDIR . '/packages/temp');
756
		}
757
758
		// Log what we just did.
759
		logAction($this->_uninstalling ? 'uninstall_package' : (empty($is_upgrade) ? 'install_package' : 'upgrade_package'), ['package' => Util::htmlspecialchars($packageInfo['name']), 'version' => Util::htmlspecialchars($packageInfo['version'])], 'admin');
760
761
		// Just in case, let's clear the whole cache to avoid anything going up the swanny.
762
		Cache::instance()->clean();
763
764
		// Restore file permissions?
765
		$chmod_control = new PackageChmod();
766
		$chmod_control->createChmodControl([], [], true);
767
	}
768
769
	/**
770
	 * Get Custom Themes
771
	 *
772
	 * This method extracts custom themes from the requests and validates them.
773
	 *
774
	 * @return array
775
	 */
776
	private function _getCustomThemes(): array
777
	{
778
		global $modSettings;
779
780
		$custom_themes = [1];
781
		$known_themes = explode(',', $modSettings['knownThemes']);
782
		$known_themes = array_map('intval', $known_themes);
783
784
		if (!empty($this->_req->post->custom_theme))
785
		{
786
			// Normalize to array to avoid invalid foreach or string offset issues
787
			$custom_theme = $this->_req->post->custom_theme;
788
			if (!is_array($custom_theme))
789
			{
790
				$custom_theme = [$custom_theme];
791
			}
792
793
			foreach ($custom_theme as $tid)
794
			{
795
				if (in_array($tid, $known_themes, true))
796
				{
797
					$custom_themes[] = $tid;
798
				}
799
			}
800
		}
801
802
		return $custom_themes;
803
	}
804
805
	/**
806
	 * Install Themes
807
	 *
808
	 * This method installs the custom themes.
809
	 *
810
	 * @return array
811
	 */
812
	private function _installThemes(): array
813
	{
814
		global $context;
815
816
		$themes_installed = [1];
817
818
		$context['theme_copies'] = [
819
			'require-file' => [],
820
			'require-dir' => [],
821
		];
822
823
		if (!empty($this->_req->post->theme_changes))
824
		{
825
			// Normalize to array to avoid invalid foreach or string offset issues
826
			$theme_changes = $this->_req->post->theme_changes;
827
			if (!is_array($theme_changes))
828
			{
829
				$theme_changes = [$theme_changes];
830
			}
831
832
			foreach ($theme_changes as $change)
833
			{
834
				if (empty($change))
835
				{
836
					continue;
837
				}
838
839
				$theme_data = json_decode(base64_decode($change), true);
840
841
				if (empty($theme_data['type']))
842
				{
843
					continue;
844
				}
845
846
				$themes_installed[] = (int) $theme_data['id'];
847
				$context['theme_copies'][$theme_data['type']][$theme_data['orig']][] = $theme_data['future'];
848
			}
849
		}
850
851
		return $themes_installed;
852
	}
853
854
	/**
855
	 * List the files in a package.
856
	 */
857
	public function action_list(): void
858
	{
859
		global $txt, $context;
860
861
		// No package?  Show him or her the door.
862
		$package = $this->_req->getQuery('package', 'trim', '');
863
		if (empty($package))
864
		{
865
			redirectexit('action=admin;area=packages');
866
		}
867
868
		$context['breadcrumbs'][] = [
869
			'url' => getUrl('admin', ['action' => 'admin', 'area' => 'packages', 'sa' => 'list', 'package' => $package]),
870
			'name' => $txt['list_file']
871
		];
872
		$context['page_title'] .= ' - ' . $txt['list_file'];
873
		$context['sub_template'] = 'list';
874
875
		// The filename...
876
		$context['filename'] = $package;
877
878
		// Let the unpacker do the work.
879
		if (is_file(BOARDDIR . '/packages/' . $context['filename']))
880
		{
881
			$context['files'] = read_tgz_file(BOARDDIR . '/packages/' . $context['filename'], null);
882
		}
883
		elseif ($this->fileFunc->isDir(BOARDDIR . '/packages/' . $context['filename']))
884
		{
885
			$context['files'] = $this->fileFunc->listtree(BOARDDIR . '/packages/' . $context['filename']);
886
		}
887
	}
888
889
	/**
890
	 * Display one of the files in a package.
891
	 */
892
	public function action_examine(): void
893
	{
894
		global $txt, $context;
895
896
		// No package?  Show him or her the door.
897
		if (!$this->_req->hasQuery('package') || $this->_req->query->package == '')
898
		{
899
			redirectexit('action=admin;area=packages');
900
		}
901
902
		// No file?  Show him or her the door.
903
		if (empty($this->_req->getQuery('file', 'trim', '')))
904
		{
905
			redirectexit('action=admin;area=packages');
906
		}
907
908
		$this->_req->query->package = preg_replace('~[.]+~', '.', strtr($this->_req->query->package, ['/' => '_', '\\' => '_']));
909
		$this->_req->query->file = preg_replace('~[.]+~', '.', $this->_req->query->file);
910
911
		if ($this->_req->hasQuery('raw'))
912
		{
913
			if (is_file(BOARDDIR . '/packages/' . $this->_req->query->package))
914
			{
915
				echo read_tgz_file(BOARDDIR . '/packages/' . $this->_req->query->package, $this->_req->query->file, true);
0 ignored issues
show
Bug introduced by
Are you sure read_tgz_file(ElkArte\Pa...req->query->file, true) of type array|boolean can be used in echo? ( Ignorable by Annotation )

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

915
				echo /** @scrutinizer ignore-type */ read_tgz_file(BOARDDIR . '/packages/' . $this->_req->query->package, $this->_req->query->file, true);
Loading history...
916
			}
917
			elseif ($this->fileFunc->isDir(BOARDDIR . '/packages/' . $this->_req->query->package))
918
			{
919
				echo file_get_contents(BOARDDIR . '/packages/' . $this->_req->query->package . '/' . $this->_req->query->file);
920
			}
921
922
			obExit(false);
923
		}
924
925
		$context['breadcrumbs'][count($context['breadcrumbs']) - 1] = [
926
			'url' => getUrl('admin', ['action' => 'admin', 'area' => 'packages', 'sa' => 'list', 'package' => $this->_req->query->package]),
927
			'name' => $txt['package_examine_file']
928
		];
929
		$context['page_title'] .= ' - ' . $txt['package_examine_file'];
930
		$context['sub_template'] = 'examine';
931
932
		// The filename...
933
		$context['package'] = $this->_req->query->package;
934
		$context['filename'] = $this->_req->query->file;
935
936
		// Let the unpacker do the work... but make sure we handle images properly.
937
		if (in_array(strtolower(strrchr($this->_req->query->file, '.')), ['.bmp', '.gif', '.jpeg', '.jpg', '.png']))
938
		{
939
			$context['filedata'] = '<img src="' . getUrl('admin', ['action' => 'admin', 'area' => 'packages', 'sa' => 'examine', 'package' => $this->_req->query->package, 'file' => $this->_req->query->file, 'raw']) . '" alt="' . $this->_req->query->file . '" />';
940
		}
941
		elseif (is_file(BOARDDIR . '/packages/' . $this->_req->query->package))
942
		{
943
			$context['filedata'] = htmlspecialchars(read_tgz_file(BOARDDIR . '/packages/' . $this->_req->query->package, $this->_req->query->file, true));
0 ignored issues
show
Bug introduced by
read_tgz_file(ElkArte\Pa...req->query->file, true) of type array|boolean is incompatible with the type string expected by parameter $string of htmlspecialchars(). ( Ignorable by Annotation )

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

943
			$context['filedata'] = htmlspecialchars(/** @scrutinizer ignore-type */ read_tgz_file(BOARDDIR . '/packages/' . $this->_req->query->package, $this->_req->query->file, true));
Loading history...
944
		}
945
		elseif ($this->fileFunc->isDir(BOARDDIR . '/packages/' . $this->_req->query->package))
946
		{
947
			$context['filedata'] = htmlspecialchars(file_get_contents(BOARDDIR . '/packages/' . $this->_req->query->package . '/' . $this->_req->query->file));
948
		}
949
	}
950
951
	/**
952
	 * Empty out the installed list.
953
	 */
954
	public function action_flush(): void
955
	{
956
		// Always check the session.
957
		checkSession('get');
958
959
		include_once(SUBSDIR . '/Package.subs.php');
960
961
		// Record when we last did this.
962
		package_put_contents(BOARDDIR . '/packages/installed.list', time());
963
964
		// Set everything as uninstalled.
965
		setPackagesAsUninstalled();
966
967
		redirectexit('action=admin;area=packages;sa=installed');
968
	}
969
970
	/**
971
	 * Delete a package.
972
	 */
973
	public function action_remove(): void
974
	{
975
		// Check it.
976
		checkSession('get');
977
978
		// Ack, don't allow deletion of arbitrary files here, could become a security hole somehow!
979
		$check = $this->_req->getQuery('package', 'trim', '');
980
		if (empty($check) || $check === 'index.php' || $check === 'installed.list' || $check === 'backups')
981
		{
982
			redirectexit('action=admin;area=packages;sa=browse');
983
		}
984
985
		$check = preg_replace('~[\.]+~', '.', strtr($check, ['/' => '_', '\\' => '_']));
986
987
		// Can't delete what's not there.
988
		if ($this->fileFunc->fileExists(BOARDDIR . '/packages/' . $check)
989
			&& (str_ends_with($check, '.zip')
990
				|| str_ends_with($check, '.tgz')
991
				|| str_ends_with($check, '.tar.gz')
992
				|| $this->fileFunc->isDir(BOARDDIR . '/packages/' . $check))
993
			&& $check !== 'backups'
994
			&& $check[0] !== '.')
995
		{
996
			$chmod_control = new PackageChmod();
997
			$chmod_control->createChmodControl(
998
				[BOARDDIR . '/packages/' . $check],
999
				[
1000
					'destination_url' => getUrl('admin', ['action' => 'admin', 'area' => 'packages', 'sa' => 'remove', 'package' => $check]),
1001
					'crash_on_error' => true
1002
				]
1003
			);
1004
1005
			if ($this->fileFunc->isDir(BOARDDIR . '/packages/' . $check))
1006
			{
1007
				deltree(BOARDDIR . '/packages/' . $check);
1008
			}
1009
			else
1010
			{
1011
				$this->fileFunc->chmod(BOARDDIR . '/packages/' . $check);
1012
				$this->fileFunc->delete(BOARDDIR . '/packages/' . $check);
1013
			}
1014
		}
1015
1016
		redirectexit('action=admin;area=packages;sa=browse');
1017
	}
1018
1019
	/**
1020
	 * Browse a list of packages.
1021
	 */
1022
	public function action_browse(): void
1023
	{
1024
		global $txt, $context;
1025
1026
		$context['page_title'] .= ' - ' . $txt['browse_packages'];
1027
		$context['forum_version'] = FORUM_VERSION;
1028
		$context['available_addon'] = [];
1029
		$context['available_avatar'] = [];
1030
		$context['available_smiley'] = [];
1031
		$context['available_language'] = [];
1032
		$context['available_unknown'] = [];
1033
1034
		$context['package_types'] = ['addon', 'avatar', 'language', 'smiley', 'unknown'];
1035
1036
		call_integration_hook('integrate_package_types');
1037
1038
		foreach ($context['package_types'] as $type)
1039
		{
1040
			// Use the standard templates for showing this.
1041
			$listOptions = [
1042
				'id' => 'packages_lists_' . $type,
1043
				'title' => $txt[($type === 'addon' ? 'modification' : $type) . '_package'],
1044
				'no_items_label' => $txt['no_packages'],
1045
				'get_items' => [
1046
					'function' => fn(int $start, int $items_per_page, string $sort, string $params) => $this->list_packages($start, $items_per_page, $sort, $params),
1047
					'params' => ['params' => $type],
1048
				],
1049
				'base_href' => getUrl('admin', ['action' => 'admin', 'area' => 'packages', 'sa' => $context['sub_action'], 'type' => $type]),
1050
				'default_sort_col' => 'pkg_name' . $type,
1051
				'columns' => [
1052
					'pkg_name' . $type => [
1053
						'header' => [
1054
							'value' => $txt['mod_name'],
1055
							'style' => 'width: 25%;',
1056
						],
1057
						'data' => [
1058
							'function' => static function ($package_md5) use ($type) {
1059
								global $context;
1060
1061
								if (isset($context['available_' . $type][$package_md5]))
1062
								{
1063
									return $context['available_' . $type][$package_md5]['name'];
1064
								}
1065
1066
								return '';
1067
							},
1068
						],
1069
						'sort' => [
1070
							'default' => 'name',
1071
							'reverse' => 'name',
1072
						],
1073
					],
1074
					'version' . $type => [
1075
						'header' => [
1076
							'value' => $txt['mod_version'],
1077
							'style' => 'width: 25%;',
1078
						],
1079
						'data' => [
1080
							'function' => static function ($package_md5) use ($type) {
1081
								global $context;
1082
1083
								if (isset($context['available_' . $type][$package_md5]))
1084
								{
1085
									return $context['available_' . $type][$package_md5]['version'];
1086
								}
1087
1088
								return '';
1089
							},
1090
						],
1091
						'sort' => [
1092
							'default' => 'version',
1093
							'reverse' => 'version',
1094
						],
1095
					],
1096
					'time_installed' . $type => [
1097
						'header' => [
1098
							'value' => $txt['package_installed_on'],
1099
						],
1100
						'data' => [
1101
							'function' => static function ($package_md5) use ($type, $txt) {
1102
								global $context;
1103
1104
								if (!empty($context['available_' . $type][$package_md5]['time_installed']))
1105
								{
1106
									return htmlTime($context['available_' . $type][$package_md5]['time_installed']);
1107
								}
1108
1109
								return $txt['not_applicable'];
1110
							},
1111
						],
1112
						'sort' => [
1113
							'default' => 'time_installed',
1114
							'reverse' => 'time_installed',
1115
						],
1116
					],
1117
					'operations' . $type => [
1118
						'header' => [
1119
							'value' => '',
1120
						],
1121
						'data' => [
1122
							'function' => static function ($package_md5) use ($type) {
1123
								global $context, $txt;
1124
1125
								if (!isset($context['available_' . $type][$package_md5]))
1126
								{
1127
									return '';
1128
								}
1129
1130
								// Rewrite shortcut
1131
								$package = $context['available_' . $type][$package_md5];
1132
								$return = '';
1133
								if ($package['can_uninstall'])
1134
								{
1135
									$return = '
1136
										<a class="linkbutton" href="' . getUrl('admin', ['action' => 'admin', 'area' => 'packages', 'sa' => 'uninstall', 'package' => $package['filename'], 'pid' => $package['installed_id']]) . '">' . $txt['uninstall'] . '</a>';
1137
								}
1138
								elseif ($package['can_emulate_uninstall'])
1139
								{
1140
									$return = '
1141
										<a class="linkbutton" href="' . getUrl('admin', ['action' => 'admin', 'area' => 'packages', 'sa' => 'uninstall', 've' => $package['can_emulate_uninstall'], 'package' => $package['filename'], 'pid' => $package['installed_id']]) . '">' . $txt['package_emulate_uninstall'] . ' ' . $package['can_emulate_uninstall'] . '</a>';
1142
								}
1143
								elseif ($package['can_upgrade'])
1144
								{
1145
									$return = '
1146
										<a class="linkbutton" href="' . getUrl('admin', ['action' => 'admin', 'area' => 'packages', 'sa' => 'install', 'package' => $package['filename']]) . '">' . $txt['package_upgrade'] . '</a>';
1147
								}
1148
								elseif ($package['can_install'])
1149
								{
1150
									$return = '
1151
										<a class="linkbutton" href="' . getUrl('admin', ['action' => 'admin', 'area' => 'packages', 'sa' => 'install', 'package' => $package['filename']]) . '">' . $txt['install_mod'] . '</a>';
1152
								}
1153
								elseif ($package['can_emulate_install'])
1154
								{
1155
									$return = '
1156
										<a class="linkbutton" href="' . getUrl('admin', ['action' => 'admin', 'area' => 'packages', 'sa' => 'install', 've' => $package['can_emulate_install'], 'package' => $package['filename']]) . '">' . $txt['package_emulate_install'] . ' ' . $package['can_emulate_install'] . '</a>';
1157
								}
1158
								return $return . '
1159
										<a class="linkbutton" href="' . getUrl('admin', ['action' => 'admin', 'area' => 'packages', 'sa' => 'list', 'package' => $package['filename']]) . '">' . $txt['list_files'] . '</a>
1160
										<a class="linkbutton" href="' . getUrl('admin', ['action' => 'admin', 'area' => 'packages', 'sa' => 'remove', 'package' => $package['filename'], '{session_data}']) . '"' . ($package['is_installed'] && $package['is_current']
1161
										? ' onclick="return confirm(\'' . $txt['package_delete_bad'] . '\');"'
1162
										: '') . '>' . $txt['package_delete'] . '</a>';
1163
							},
1164
							'class' => 'righttext',
1165
						],
1166
					],
1167
				],
1168
				'additional_rows' => [
1169
					[
1170
						'position' => 'bottom_of_list',
1171
						'class' => 'submitbutton',
1172
						'value' => ($context['sub_action'] === 'browse'
1173
							? ''
1174
							: '<a class="linkbutton" href="' . getUrl('admin', ['action' => 'admin', 'area' => 'packages', 'sa' => 'flush', '{session_data}']) . '" onclick="return confirm(\'' . $txt['package_delete_list_warning'] . '\');">' . $txt['delete_list'] . '</a>'),
1175
					],
1176
				],
1177
			];
1178
1179
			createList($listOptions);
1180
		}
1181
1182
		$context['sub_template'] = 'browse';
1183
		$context['default_list'] = 'packages_lists';
1184
	}
1185
1186
	/**
1187
	 * Test an FTP connection via Ajax
1188
	 *
1189
	 * @uses Xml Template, generic_xml sub template
1190
	 */
1191
	public function action_ftptest(): void
1192
	{
1193
		global $context, $txt, $package_ftp;
1194
1195
		checkSession('get');
1196
1197
		// Try to make the FTP connection.
1198
		$chmod_control = new PackageChmod();
1199
		$chmod_control->createChmodControl([], ['force_find_error' => true]);
1200
1201
		// Deal with the template stuff.
1202
		theme()->getTemplates()->load('Xml');
1203
		$context['sub_template'] = 'generic_xml';
1204
		theme()->getLayers()->removeAll();
1205
1206
		// Define the return data, this is simple.
1207
		$context['xml_data'] = [
1208
			'results' => [
1209
				'identifier' => 'result',
1210
				'children' => [
1211
					[
1212
						'attributes' => [
1213
							'success' => empty($package_ftp) ? 0 : 1,
1214
						],
1215
						'value' => empty($package_ftp)
1216
							? $context['package_ftp']['error'] ?? $txt['package_ftp_test_failed']
1217
							: ($txt['package_ftp_test_success']),
1218
					],
1219
				],
1220
			],
1221
		];
1222
	}
1223
1224
	/**
1225
	 * Used when temp FTP access is needed to package functions
1226
	 */
1227
	public function action_options(): void
1228
	{
1229
		global $txt, $context, $modSettings;
1230
1231
		if ($this->_req->hasPost('save'))
1232
		{
1233
			checkSession();
1234
1235
			updateSettings([
1236
				'package_server' => $this->_req->getPost('pack_server', 'trim|Util::htmlspecialchars'),
1237
				'package_port' => $this->_req->getPost('pack_port', 'trim|Util::htmlspecialchars'),
1238
				'package_username' => $this->_req->getPost('pack_user', 'trim|Util::htmlspecialchars'),
1239
				'package_make_backups' => !empty($this->_req->post->package_make_backups),
1240
				'package_make_full_backups' => !empty($this->_req->post->package_make_full_backups)
1241
			]);
1242
1243
			redirectexit('action=admin;area=packages;sa=options');
1244
		}
1245
1246
		if (preg_match('~^/home\d*/([^/]+?)/public_html~', $this->_req->server->DOCUMENT_ROOT, $match))
1247
		{
1248
			$default_username = $match[1];
1249
		}
1250
		else
1251
		{
1252
			$default_username = '';
1253
		}
1254
1255
		$context['page_title'] = $txt['package_settings'];
1256
		$context['sub_template'] = 'install_options';
1257
		$context['package_ftp_server'] = $modSettings['package_server'] ?? 'localhost';
1258
		$context['package_ftp_port'] = $modSettings['package_port'] ?? '21';
1259
		$context['package_ftp_username'] = $modSettings['package_username'] ?? $default_username;
1260
		$context['package_make_backups'] = !empty($modSettings['package_make_backups']);
1261
		$context['package_make_full_backups'] = !empty($modSettings['package_make_full_backups']);
1262
	}
1263
1264
	/**
1265
	 * List operations
1266
	 */
1267
	public function action_showoperations(): void
1268
	{
1269
		global $context, $txt;
1270
1271
		// Can't be in here buddy.
1272
		isAllowedTo('admin_forum');
1273
1274
		$operation_key = $this->_req->getQuery('operation_key', 'trim');
1275
		$filename = $this->_req->getQuery('filename', 'trim');
1276
		$package = $this->_req->getQuery('package', 'trim');
1277
		$install_id = $this->_req->getQuery('install_id', 'intval', 0);
1278
1279
		// We need to know the operation key for the search and replace?
1280
		if (!isset($operation_key, $filename) && !is_numeric($operation_key))
1281
		{
1282
			throw new Exception('operation_invalid', 'general');
1283
		}
1284
1285
		// Load the required file.
1286
		require_once(SUBSDIR . '/Themes.subs.php');
1287
1288
		// Uninstalling the mod?
1289
		$reverse = $this->_req->hasQuery('reverse');
1290
1291
		// Get the base name.
1292
		$context['filename'] = preg_replace('~[\.]+~', '.', $package);
1293
1294
		// We need to extract this again.
1295
		if (is_file(BOARDDIR . '/packages/' . $context['filename']))
1296
		{
1297
			$this->_extracted_files = read_tgz_file(BOARDDIR . '/packages/' . $context['filename'], BOARDDIR . '/packages/temp');
1298
			if ($this->_extracted_files
1299
				&& !$this->fileFunc->fileExists(BOARDDIR . '/packages/temp/package-info.xml'))
1300
			{
1301
				foreach ($this->_extracted_files as $file)
1302
				{
1303
					if (basename($file['filename']) === 'package-info.xml')
1304
					{
1305
						$this->_base_path = dirname($file['filename']) . '/';
1306
						break;
1307
					}
1308
				}
1309
			}
1310
1311
			if ($this->_base_path === null)
1312
			{
1313
				$this->_base_path = '';
1314
			}
1315
		}
1316
		elseif ($this->fileFunc->isDir(BOARDDIR . '/packages/' . $context['filename']))
1317
		{
1318
			copytree(BOARDDIR . '/packages/' . $context['filename'], BOARDDIR . '/packages/temp');
1319
			$this->_extracted_files = $this->fileFunc->listtree(BOARDDIR . '/packages/temp');
1320
			$this->_base_path = '';
1321
		}
1322
1323
		$context['base_path'] = $this->_base_path;
1324
		$context['extracted_files'] = $this->_extracted_files;
1325
1326
		// Load up any custom themes we may want to install into...
1327
		$theme_paths = getThemesPathbyID();
1328
1329
		// For uninstall operations we only consider the themes in which the package is installed.
1330
		if ($reverse && !empty($install_id) && $install_id > 0)
1331
		{
1332
			$old_themes = loadThemesAffected($install_id);
1333
			foreach ($theme_paths as $id => $data)
1334
			{
1335
				if ((int) $id === 1)
1336
				{
1337
					continue;
1338
				}
1339
1340
				if (in_array($id, $old_themes))
1341
				{
1342
					continue;
1343
				}
1344
1345
				unset($theme_paths[$id]);
1346
			}
1347
		}
1348
1349
		$mod_actions = parseModification(@file_get_contents(BOARDDIR . '/packages/temp/' . $context['base_path'] . $this->_req->query->filename), true, $reverse, $theme_paths);
0 ignored issues
show
Bug introduced by
It seems like @file_get_contents(ElkAr...>_req->query->filename) can also be of type false; however, parameter $file of parseModification() 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

1349
		$mod_actions = parseModification(/** @scrutinizer ignore-type */ @file_get_contents(BOARDDIR . '/packages/temp/' . $context['base_path'] . $this->_req->query->filename), true, $reverse, $theme_paths);
Loading history...
1350
1351
		// Ok, let's get the content of the file.
1352
		$context['operations'] = [
1353
			'search' => strtr(htmlspecialchars($mod_actions[$operation_key]['search_original'], ENT_COMPAT), ['[' => '&#91;', ']' => '&#93;']),
1354
			'replace' => strtr(htmlspecialchars($mod_actions[$operation_key]['replace_original'], ENT_COMPAT), ['[' => '&#91;', ']' => '&#93;']),
1355
			'position' => $mod_actions[$operation_key]['position'],
1356
		];
1357
1358
		// Let's do some formatting...
1359
		$operation_text = $context['operations']['position'] === 'replace' ? 'operation_replace' : ($context['operations']['position'] === 'before' ? 'operation_after' : 'operation_before');
1360
		$bbc_parser = ParserWrapper::instance();
1361
		$context['operations']['search'] = $bbc_parser->parsePackage('[code=' . $txt['operation_find'] . ']' . ($context['operations']['position'] === 'end' ? '?&gt;' : $context['operations']['search']) . '[/code]');
1362
		$context['operations']['replace'] = $bbc_parser->parsePackage('[code=' . $txt[$operation_text] . ']' . $context['operations']['replace'] . '[/code]');
1363
1364
		// No layers
1365
		theme()->getLayers()->removeAll();
1366
		$context['sub_template'] = 'view_operations';
1367
	}
1368
1369
	/**
1370
	 * Get a listing of all the packages
1371
	 *
1372
	 * - Determines if the package is addon, smiley, avatar, language, or unknown package
1373
	 * - Determines if the package has been installed or not
1374
	 *
1375
	 * @param int $start The item to start with (for pagination purposes)
1376
	 * @param int $items_per_page The number of items to show per page
1377
	 * @param string $sort A string indicating how to sort the results
1378
	 * @param string $params 'type' type of package
1379
	 *
1380
	 * @return mixed
1381
	 * @throws Exception
1382
	 */
1383
	public function list_packages($start, $items_per_page, $sort, $params)
1384
	{
1385
		global $context;
1386
		static $instadds, $packages;
1387
1388
		// Start things up
1389
		if (!isset($packages[$params]))
1390
		{
1391
			$packages[$params] = [];
1392
		}
1393
1394
		// We need the packages directory to be writable for this.
1395
		if (!$this->fileFunc->isWritable(BOARDDIR . '/packages'))
1396
		{
1397
			$create_chmod_control = new PackageChmod();
1398
			$create_chmod_control->createChmodControl(
1399
				[BOARDDIR . '/packages'],
1400
				[
1401
					'destination_url' => getUrl('admin', ['action' => 'admin', 'area' => 'packages']),
1402
					'crash_on_error' => true
1403
				]
1404
			);
1405
		}
1406
1407
		[$the_brand, $the_version] = explode(' ', FORUM_VERSION, 2);
1408
1409
		// Here we have a little code to help those who class themselves as something of gods, version emulation ;)
1410
		$checkEmulate = $this->_req->getQuery('version_emulate', 'trim', '');
1411
		if (!empty($checkEmulate) && strtr($checkEmulate, [$the_brand => '']) === $the_version)
1412
		{
1413
			unset($_SESSION['version_emulate']);
1414
		}
1415
		elseif ($checkEmulate)
1416
		{
1417
			if (($checkEmulate === FORUM_VERSION) && isset($_SESSION['version_emulate']))
1418
			{
1419
				unset($_SESSION['version_emulate']);
1420
			}
1421
			elseif ($checkEmulate !== 0)
1422
			{
1423
				$_SESSION['version_emulate'] = strtr($checkEmulate, ['-' => ' ', '+' => ' ', $the_brand . ' ' => '']);
1424
			}
1425
		}
1426
1427
		if (!empty($_SESSION['version_emulate']))
1428
		{
1429
			$context['forum_version'] = $the_brand . ' ' . $_SESSION['version_emulate'];
1430
			$the_version = $_SESSION['version_emulate'];
1431
		}
1432
1433
		if (isset($_SESSION['single_version_emulate']))
1434
		{
1435
			unset($_SESSION['single_version_emulate']);
1436
		}
1437
1438
		if (empty($instadds))
1439
		{
1440
			$instadds = loadInstalledPackages();
1441
			$installed_adds = [];
1442
1443
			// Look through the list of installed mods...
1444
			foreach ($instadds as $installed_add)
1445
			{
1446
				$installed_adds[$installed_add['package_id']] = [
1447
					'id' => $installed_add['id'],
1448
					'version' => $installed_add['version'],
1449
					'time_installed' => $installed_add['time_installed'],
1450
				];
1451
			}
1452
1453
			// Get a list of all the ids installed, so the latest packages won't include already installed ones.
1454
			$context['installed_adds'] = array_keys($installed_adds);
1455
		}
1456
1457
		if (empty($packages))
1458
		{
1459
			foreach ($context['package_types'] as $type)
1460
			{
1461
				$packages[$type] = [];
1462
			}
1463
		}
1464
1465
		try
1466
		{
1467
			$dir = new FilesystemIterator(BOARDDIR . '/packages', FilesystemIterator::SKIP_DOTS);
1468
			$filtered_dir = new PackagesFilterIterator($dir);
1469
1470
			$dirs = [];
1471
			$sort_id = [
1472
				'addon' => 1,
1473
				'avatar' => 1,
1474
				'language' => 1,
1475
				'smiley' => 1,
1476
				'unknown' => 1,
1477
			];
1478
			foreach ($filtered_dir as $package)
1479
			{
1480
				foreach ($context['package_types'] as $type)
1481
				{
1482
					if (isset($context['available_' . $type][md5($package->getFilename())]))
1483
					{
1484
						continue 2;
1485
					}
1486
				}
1487
1488
				// Skip directories or files that are named the same.
1489
				if ($package->isDir())
1490
				{
1491
					if (in_array($package, $dirs, true))
1492
					{
1493
						continue;
1494
					}
1495
1496
					$dirs[] = $package;
1497
				}
1498
				elseif (strtolower(substr($package->getFilename(), -7)) === '.tar.gz')
1499
				{
1500
					if (in_array(substr($package, 0, -7), $dirs, true))
1501
					{
1502
						continue;
1503
					}
1504
1505
					$dirs[] = substr($package, 0, -7);
1506
				}
1507
				elseif (strtolower($package->getExtension()) === 'zip' || strtolower($package->getExtension()) === 'tgz')
1508
				{
1509
					if (in_array(substr($package->getBasename(), 0, -4), $dirs, true))
1510
					{
1511
						continue;
1512
					}
1513
1514
					$dirs[] = substr($package->getBasename(), 0, -4);
1515
				}
1516
1517
				$packageInfo = getPackageInfo($package->getFilename());
1518
				if (!is_array($packageInfo) || empty($packageInfo))
1519
				{
1520
					continue;
1521
				}
1522
1523
				$packageInfo['installed_id'] = isset($installed_adds[$packageInfo['id']]) ? $installed_adds[$packageInfo['id']]['id'] : 0;
1524
				$packageInfo['sort_id'] = $sort_id[$packageInfo['type']] ?? $sort_id['unknown'];
1525
				$packageInfo['is_installed'] = isset($installed_adds[$packageInfo['id']]);
1526
				$packageInfo['is_current'] = $packageInfo['is_installed'] && isset($installed_adds[$packageInfo['id']]) && ($installed_adds[$packageInfo['id']]['version'] == $packageInfo['version']);
1527
				$packageInfo['is_newer'] = $packageInfo['is_installed'] && isset($installed_adds[$packageInfo['id']]) && ($installed_adds[$packageInfo['id']]['version'] > $packageInfo['version']);
1528
				$packageInfo['can_install'] = false;
1529
				$packageInfo['can_uninstall'] = false;
1530
				$packageInfo['can_upgrade'] = false;
1531
				$packageInfo['can_emulate_install'] = false;
1532
				$packageInfo['can_emulate_uninstall'] = false;
1533
				$packageInfo['time_installed'] = $installed_adds[$packageInfo['id']]['time_installed'] ?? 0;
1534
1535
				// This package is currently NOT installed.  Check if it can be.
1536
				if (!$packageInfo['is_installed'] && $packageInfo['xml']->exists('install'))
1537
				{
1538
					// Check if there's an installation for *THIS* version
1539
					$installs = $packageInfo['xml']->set('install');
1540
					$packageInfo['time_installed'] = 0;
1541
					foreach ($installs as $install)
1542
					{
1543
						if (!$install->exists('@for') || matchPackageVersion($the_version, $install->fetch('@for')))
1544
						{
1545
							// Okay, this one is good to go.
1546
							$packageInfo['can_install'] = true;
1547
							break;
1548
						}
1549
					}
1550
1551
					// no install found for our version, let's see if one exists for another
1552
					if ($packageInfo['can_install'] === false && $install->exists('@for') && empty($_SESSION['version_emulate']))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $install does not seem to be defined for all execution paths leading up to this point.
Loading history...
1553
					{
1554
						$reset = true;
1555
1556
						// Get the highest installed version that is available from the package
1557
						foreach ($installs as $install)
1558
						{
1559
							$packageInfo['can_emulate_install'] = matchHighestPackageVersion($install->fetch('@for'), $the_version, $reset);
1560
							$reset = false;
1561
						}
1562
					}
1563
				}
1564
				// An already installed, but old, package.  Can we upgrade it?
1565
				elseif ($packageInfo['is_installed'] && !$packageInfo['is_current'] && $packageInfo['xml']->exists('upgrade'))
1566
				{
1567
					$upgrades = $packageInfo['xml']->set('upgrade');
1568
1569
					// First, go through and check against the current version of ElkArte.
1570
					foreach ($upgrades as $upgrade)
1571
					{
1572
						// Even if it is for this ElkArte, is it for the installed version of the mod?
1573
						if ($upgrade->exists('@for') && !matchPackageVersion($the_version, $upgrade->fetch('@for')))
1574
						{
1575
							continue;
1576
						}
1577
1578
						if ($upgrade->exists('@from') && !matchPackageVersion($installed_adds[$packageInfo['id']]['version'], $upgrade->fetch('@from')))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $installed_adds does not seem to be defined for all execution paths leading up to this point.
Loading history...
1579
						{
1580
							continue;
1581
						}
1582
1583
						$packageInfo['can_upgrade'] = true;
1584
						break;
1585
					}
1586
				}
1587
				// Note that it has to be the current version to be uninstallable.  Shucks.
1588
				elseif ($packageInfo['is_installed'] && $packageInfo['is_current'] && $packageInfo['xml']->exists('uninstall'))
1589
				{
1590
					$uninstalls = $packageInfo['xml']->set('uninstall');
1591
1592
					// Can we find any uninstallation methods that work for this ElkArte version?
1593
					foreach ($uninstalls as $uninstall)
1594
					{
1595
						if (!$uninstall->exists('@for') || matchPackageVersion($the_version, $uninstall->fetch('@for')))
1596
						{
1597
							$packageInfo['can_uninstall'] = true;
1598
							break;
1599
						}
1600
					}
1601
1602
					// No uninstall found for this version, let's see if one exists for another
1603
					if ($packageInfo['can_uninstall'] === false && $uninstall->exists('@for') && empty($_SESSION['version_emulate']))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $uninstall does not seem to be defined for all execution paths leading up to this point.
Loading history...
1604
					{
1605
						$reset = true;
1606
1607
						// Get the highest installed version that is available from the package
1608
						foreach ($uninstalls as $uninstall)
1609
						{
1610
							$packageInfo['can_emulate_uninstall'] = matchHighestPackageVersion($uninstall->fetch('@for'), $the_version, $reset);
1611
							$reset = false;
1612
						}
1613
					}
1614
				}
1615
1616
				unset($packageInfo['xml']);
1617
1618
				// Add-on / Modification
1619
				if ($packageInfo['type'] === 'addon' || $packageInfo['type'] === 'modification' || $packageInfo['type'] === 'mod')
1620
				{
1621
					$sort_id['addon']++;
1622
					$packages['addon'][strtolower($packageInfo[$sort]) . '_' . $sort_id['addon']] = md5($package->getFilename());
1623
					$context['available_addon'][md5($package->getFilename())] = $packageInfo;
1624
				}
1625
				// Avatar package.
1626
				elseif ($packageInfo['type'] === 'avatar')
1627
				{
1628
					$sort_id[$packageInfo['type']]++;
1629
					$packages['avatar'][strtolower($packageInfo[$sort]) . '_' . $sort_id['avatar']] = md5($package->getFilename());
1630
					$context['available_avatar'][md5($package->getFilename())] = $packageInfo;
1631
				}
1632
				// Smiley package.
1633
				elseif ($packageInfo['type'] === 'smiley')
1634
				{
1635
					$sort_id[$packageInfo['type']]++;
1636
					$packages['smiley'][strtolower($packageInfo[$sort]) . '_' . $sort_id['smiley']] = md5($package->getFilename());
1637
					$context['available_smiley'][md5($package->getFilename())] = $packageInfo;
1638
				}
1639
				// Language package.
1640
				elseif ($packageInfo['type'] === 'language')
1641
				{
1642
					$sort_id[$packageInfo['type']]++;
1643
					$packages['language'][strtolower($packageInfo[$sort]) . '_' . $sort_id['language']] = md5($package->getFilename());
1644
					$context['available_language'][md5($package->getFilename())] = $packageInfo;
1645
				}
1646
				// Other stuff.
1647
				else
1648
				{
1649
					$sort_id['unknown']++;
1650
					$packages['unknown'][strtolower($packageInfo[$sort]) . '_' . $sort_id['unknown']] = md5($package->getFilename());
1651
					$context['available_unknown'][md5($package->getFilename())] = $packageInfo;
1652
				}
1653
			}
1654
		}
1655
		catch (UnexpectedValueException)
1656
		{
1657
			// @todo for now do nothing...
1658
		}
1659
1660
		if (isset($this->_req->query->desc))
1661
		{
1662
			krsort($packages[$params]);
1663
		}
1664
		else
1665
		{
1666
			ksort($packages[$params]);
1667
		}
1668
1669
		return $packages[$params];
1670
	}
1671
1672
	/**
1673
	 * Removes database changes if specified conditions are met.
1674
	 *
1675
	 * @param array $package_installed The installed package that contains the database changes.
1676
	 * @param AbstractTable $table_installer The object responsible for modifying database tables.
1677
	 */
1678
	public function removeDatabaseChanges($package_installed, $table_installer): void
1679
	{
1680
		// If there are database changes - and they want them removed - let's do it last!
1681
		if (empty($package_installed['db_changes']))
1682
		{
1683
			return;
1684
		}
1685
		if (empty($this->_req->post->do_db_changes))
1686
		{
1687
			return;
1688
		}
1689
		foreach ($package_installed['db_changes'] as $change)
1690
		{
1691
			if ($change[0] === 'remove_table' && isset($change[1]))
1692
			{
1693
				$table_installer->drop_table($change[1]);
1694
			}
1695
			elseif ($change[0] === 'remove_column' && isset($change[2]))
1696
			{
1697
				$table_installer->remove_column($change[1], $change[2]);
1698
			}
1699
			elseif ($change[0] === 'remove_index' && isset($change[2]))
1700
			{
1701
				$table_installer->remove_index($change[1], $change[2]);
1702
			}
1703
		}
1704
	}
1705
1706
	/**
1707
	 * Determine database changes based on the given package information and installed package.
1708
	 *
1709
	 * @param array $packageInfo The package information that may contain uninstall database changes.
1710
	 * @param array $package_installed The installed package that contains database changes.
1711
	 *
1712
	 * @return void
1713
	 */
1714
	public function determineDatabaseChanges($packageInfo, $package_installed): void
1715
	{
1716
		global $context, $txt;
1717
1718
		$context['database_changes'] = [];
1719
		if (isset($packageInfo['uninstall']['database']))
1720
		{
1721
			$context['database_changes'][] = $txt['execute_database_changes'] . ' - ' . $packageInfo['uninstall']['database'];
1722
		}
1723
		elseif (!empty($package_installed['db_changes']))
1724
		{
1725
			foreach ($package_installed['db_changes'] as $change)
1726
			{
1727
				if (isset($change[2], $txt['package_db_' . $change[0]]))
1728
				{
1729
					$context['database_changes'][] = sprintf($txt['package_db_' . $change[0]], $change[1], $change[2]);
1730
				}
1731
				elseif (isset($txt['package_db_' . $change[0]]))
1732
				{
1733
					$context['database_changes'][] = sprintf($txt['package_db_' . $change[0]], $change[1]);
1734
				}
1735
				else
1736
				{
1737
					$context['database_changes'][] = $change[0] . '-' . $change[1] . (isset($change[2]) ? '-' . $change[2] : '');
1738
				}
1739
			}
1740
		}
1741
	}
1742
1743
	/**
1744
	 * Table sorting function used in usort
1745
	 *
1746
	 * @param string[] $a
1747
	 * @param string[] $b
1748
	 *
1749
	 * @return int
1750
	 */
1751
	private function _sort_table_first($a, $b): int
1752
	{
1753
		if ($a[0] === $b[0])
1754
		{
1755
			return 0;
1756
		}
1757
1758
		return $a[0] === 'remove_table' ? -1 : 1;
1759
	}
1760
}
1761