Completed
Branch development (176841)
by Elk
06:59
created

Packages.php ➔ fetchPerms__recursive()   F

Complexity

Conditions 44
Paths > 20000

Size

Total Lines 179

Duplication

Lines 11
Ratio 6.15 %

Code Coverage

Tests 0
CRAP Score 1980

Importance

Changes 0
Metric Value
cc 44
nc 37308
nop 3
dl 11
loc 179
rs 0
c 0
b 0
f 0
ccs 0
cts 138
cp 0
crap 1980

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 dev
14
 *
15
 */
16
17
namespace ElkArte\AdminController;
18
19
/**
20
 * This class is the administration package manager controller.
21
 * Its main job is to install/uninstall, allow to browse, packages.
22
 * In fact, just about everything related to addon packages, including FTP connections when necessary.
23
 *
24
 * @package Packages
25
 */
26
class Packages extends \ElkArte\AbstractController
27
{
28
	/**
29
	 * listing of files in a packages
30
	 * @var array|boolean
31
	 */
32
	private $_extracted_files;
33
34
	/**
35
	 * Filename of the package
36
	 * @var string
37
	 */
38
	private $_filename;
39
40
	/**
41
	 * Base path of the package
42
	 * @var string
43
	 */
44
	private $_base_path;
45
46
	/**
47
	 * If this is an un-install pass or not
48
	 * @var boolean
49
	 */
50
	private $_uninstalling;
51
52
	/**
53
	 * If the package is installed, previously or not
54
	 * @var boolean
55
	 */
56
	private $_is_installed;
57
58
	/**
59
	 * The id from the DB or an installed package
60
	 * @var int
61
	 */
62
	public $install_id;
63
64
	/**
65
	 * Array of installed theme paths
66
	 * @var string[]
67
	 */
68
	public $theme_paths;
69
70
	/**
71
	 * Array of files / directories that require permissions
72
	 * @var array
73
	 */
74
	public $chmod_files;
75
76
	/**
77
	 * Pre Dispatch, called before other methods.
78
	 */
79
	public function pre_dispatch()
80
	{
81
		// Generic subs for this controller
82
		require_once(SUBSDIR . '/Package.subs.php');
83
	}
84
85
	/**
86
	 * Entry point, the default method of this controller.
87
	 *
88
	 * @event integrate_sa_packages
89
	 * @see \ElkArte\AbstractController::action_index()
90
	 */
91
	public function action_index()
92
	{
93
		global $txt, $context;
94
95
		// Admins-only!
96
		isAllowedTo('admin_forum');
97
98
		// Load all the basic stuff.
99
		theme()->getTemplates()->loadLanguageFile('Packages');
100
		theme()->getTemplates()->load('Packages');
101
		loadCSSFile('admin.css');
102
		$context['page_title'] = $txt['package'];
103
104
		// Delegation makes the world... that is, the package manager go 'round.
105
		$subActions = array(
106
			'browse' => array($this, 'action_browse'),
107
			'remove' => array($this, 'action_remove'),
108
			'list' => array($this, 'action_list'),
109
			'ftptest' => array($this, 'action_ftptest'),
110
			'install' => array($this, 'action_install'),
111
			'install2' => array($this, 'action_install2'),
112
			'uninstall' => array($this, 'action_install'),
113
			'uninstall2' => array($this, 'action_install2'),
114
			'installed' => array($this, 'action_browse'),
115
			'options' => array($this, 'action_options'),
116
			'perms' => array($this, 'action_perms'),
117
			'flush' => array($this, 'action_flush'),
118
			'examine' => array($this, 'action_examine'),
119
			'showoperations' => array($this, 'action_showoperations'),
120
			// The following two belong to PackageServers,
121
			// for UI's sake moved here at least temporarily
122
			'servers' => array(
123
				'controller' => '\\ElkArte\\AdminController\\PackageServers',
124
				'function' => 'action_list'),
125
			'upload' => array(
126
				'controller' => '\\ElkArte\\AdminController\\PackageServers',
127
				'function' => 'action_upload'),
128
		);
129
130
		// Set up action/subaction stuff.
131
		$action = new \ElkArte\Action('packages');
132
133
		// Set up some tabs...
134
		$context[$context['admin_menu_name']]['tab_data'] = array(
135
			'title' => $txt['package_manager'],
136
			'description' => $txt['package_manager_desc'],
137
			'tabs' => array(
138
				'browse' => array(
139
				),
140
				'installed' => array(
141
					'description' => $txt['installed_packages_desc'],
142
				),
143
				'perms' => array(
144
					'description' => $txt['package_file_perms_desc'],
145
				),
146
				// The following two belong to PackageServers,
147
				// for UI's sake moved here at least temporarily
148
				'servers' => array(
149
					'description' => $txt['download_packages_desc'],
150
				),
151
				'upload' => array(
152
					'description' => $txt['upload_packages_desc'],
153
				),
154
				'options' => array(
155
					'description' => $txt['package_install_options_desc'],
156
				),
157
			),
158
		);
159
160
		// Work out exactly who it is we are calling. call integrate_sa_packages
161
		$subAction = $action->initialize($subActions, 'browse');
162
163
		// Set up for the template
164
		$context['sub_action'] = $subAction;
165
166
		// Lets just do it!
167
		$action->dispatch($subAction);
168
	}
169
170
	/**
171
	 * Test install a package.
172
	 */
173
	public function action_install()
174
	{
175
		global $txt, $context, $scripturl;
176
177
		// You have to specify a file!!
178
		$file = $this->_req->getQuery('package', 'trim');
179
		if (empty($file))
180
			redirectexit('action=admin;area=packages');
181
182
		// What are we trying to do
183
		$this->_filename = (string) preg_replace('~[\.]+~', '.', $file);
184
		$this->_uninstalling = $this->_req->query->sa === 'uninstall';
185
186
		// If we can't find the file, our install ends here
187 View Code Duplication
		if (!file_exists(BOARDDIR . '/packages/' . $this->_filename))
188
			throw new \ElkArte\Exceptions\Exception('package_no_file', false);
189
190
		// Do we have an existing id, for uninstalls and the like.
191
		$this->install_id = $this->_req->getQuery('pid', 'intval', 0);
192
193
		// This will be needed
194
		require_once(SUBSDIR . '/Themes.subs.php');
195
196
		// Load up the package FTP information?
197
		create_chmod_control();
198
199
		// Make sure our temp directory exists and is empty.
200 View Code Duplication
		if (file_exists(BOARDDIR . '/packages/temp'))
201
			deltree(BOARDDIR . '/packages/temp', false);
202
		else
203
			$this->_create_temp_dir();
204
205
		// Extract the files in to the temp so we can get things like the readme, etc.
206
		$this->_extract_files_temp();
207
208
		// Load up any custom themes we may want to install into...
209
		$this->theme_paths = getThemesPathbyID();
210
211
		// Get the package info...
212
		$packageInfo = getPackageInfo($this->_filename);
213
		if (!is_array($packageInfo))
214
			throw new \ElkArte\Exceptions\Exception($packageInfo);
215
216
		$packageInfo['filename'] = $this->_filename;
217
218
		// The addon isn't installed.... unless proven otherwise.
219
		$this->_is_installed = false;
220
221
		// See if it is installed?
222
		$package_installed = isPackageInstalled($packageInfo['id'], $this->install_id);
223
224
		$context['database_changes'] = array();
225
		if (isset($packageInfo['uninstall']['database']))
226
			$context['database_changes'][] = $txt['execute_database_changes'] . ' - ' . $packageInfo['uninstall']['database'];
227
		elseif (!empty($package_installed['db_changes']))
228
		{
229
			foreach ($package_installed['db_changes'] as $change)
230
			{
231
				if (isset($change[2]) && isset($txt['package_db_' . $change[0]]))
232
					$context['database_changes'][] = sprintf($txt['package_db_' . $change[0]], $change[1], $change[2]);
233
				elseif (isset($txt['package_db_' . $change[0]]))
234
					$context['database_changes'][] = sprintf($txt['package_db_' . $change[0]], $change[1]);
235
				else
236
					$context['database_changes'][] = $change[0] . '-' . $change[1] . (isset($change[2]) ? '-' . $change[2] : '');
237
			}
238
		}
239
240
		$actions = $this->_get_package_actions($package_installed, $packageInfo);
241
242
		$context['actions'] = array();
243
		$context['ftp_needed'] = false;
244
245
		// No actions found, return so we can display an error
246
		if (empty($actions))
247
			redirectexit('action=admin;area=packages');
248
249
		// Now prepare things for the template using the package actions class
250
		$pka = new PackageActions(new \ElkArte\EventManager());
251
		$pka->setUser(\ElkArte\User::$info);
252
		$pka->test_init($actions, $this->_uninstalling, $this->_base_path, $this->theme_paths);
253
254
		$context['has_failure'] = $pka->has_failure;
255
		$context['failure_details'] = $pka->failure_details;
256
		$context['actions'] = $pka->ourActions;
257
258
		// Change our last link tree item for more information on this Packages area.
259
		$context['linktree'][count($context['linktree']) - 1] = array(
260
			'url' => $scripturl . '?action=admin;area=packages;sa=browse',
261
			'name' => $this->_uninstalling ? $txt['package_uninstall_actions'] : $txt['install_actions']
262
		);
263
264
		// All things to make the template go round
265
		$context['page_title'] .= ' - ' . ($this->_uninstalling ? $txt['package_uninstall_actions'] : $txt['install_actions']);
266
		$context['sub_template'] = 'view_package';
267
		$context['filename'] = $this->_filename;
268
		$context['package_name'] = isset($packageInfo['name']) ? $packageInfo['name'] : $this->_filename;
269
		$context['is_installed'] = $this->_is_installed;
270
		$context['uninstalling'] = $this->_uninstalling;
271
		$context['extract_type'] = isset($packageInfo['type']) ? $packageInfo['type'] : 'modification';
272
273
		// Have we got some things which we might want to do "multi-theme"?
274
		$this->_multi_theme($pka->themeFinds['candidates']);
275
276
		// Trash the cache... which will also check permissions for us!
277
		package_flush_cache(true);
278
279
		// Clear the temp directory
280
		if (file_exists(BOARDDIR . '/packages/temp'))
281
			deltree(BOARDDIR . '/packages/temp');
282
283
		// Will we need chmod permissions to pull this off
284
		$this->chmod_files = !empty($pka->chmod_files) ? $pka->chmod_files : array();
285
		if (!empty($this->chmod_files))
286
		{
287
			$ftp_status = create_chmod_control($this->chmod_files);
288
			$context['ftp_needed'] = !empty($ftp_status['files']['notwritable']) && !empty($context['package_ftp']);
289
		}
290
291
		$context['post_url'] = $scripturl . '?action=admin;area=packages;sa=' . ($this->_uninstalling ? 'uninstall' : 'install') . ($context['ftp_needed'] ? '' : '2') . ';package=' . $this->_filename . ';pid=' . $this->install_id;
292
		checkSubmitOnce('register');
293
	}
294
295
	/**
296
	 * Determines the availability / validity of installing a package in any of the installed themes
297
	 * @param array $themeFinds
298
	 */
299
	private function _multi_theme($themeFinds)
300
	{
301
		global $settings, $txt, $context;
302
303
		if (!empty($themeFinds['candidates']))
304
		{
305
			foreach ($themeFinds['candidates'] as $action_data)
306
			{
307
				// Get the part of the file we'll be dealing with.
308
				preg_match('~^\$(languagedir|languages_dir|imagesdir|themedir)(\\|/)*(.+)*~i', $action_data['unparsed_destination'], $matches);
309
310
				if ($matches[1] === 'imagesdir')
311
					$path = '/' . basename($settings['default_images_url']);
312
				elseif ($matches[1] === 'languagedir' || $matches[1] === 'languages_dir')
313
					$path = '/languages';
314
				else
315
					$path = '';
316
317
				if (!empty($matches[3]))
318
					$path .= $matches[3];
319
320
				if (!$this->_uninstalling)
321
					$path .= '/' . basename($action_data['filename']);
322
323
				// Loop through each custom theme to note it's candidacy!
324
				foreach ($this->theme_paths as $id => $theme_data)
325
				{
326
					if (isset($theme_data['theme_dir']) && $id != 1)
327
					{
328
						$real_path = $theme_data['theme_dir'] . $path;
329
330
						// Confirm that we don't already have this dealt with by another entry.
331
						if (!in_array(strtolower(strtr($real_path, array('\\' => '/'))), $themeFinds['other_themes']))
332
						{
333
							// Check if we will need to chmod this.
334
							if (!mktree(dirname($real_path), false))
335
							{
336
								$temp = dirname($real_path);
337
								while (!file_exists($temp) && strlen($temp) > 1)
338
									$temp = dirname($temp);
339
340
								$this->chmod_files[] = $temp;
341
							}
342
343
							if ($action_data['type'] === 'require-dir' && !is_writable($real_path) && (file_exists($real_path) || !is_writable(dirname($real_path))))
344
								$this->chmod_files[] = $real_path;
345
346
							if (!isset($context['theme_actions'][$id]))
347
								$context['theme_actions'][$id] = array(
348
									'name' => $theme_data['name'],
349
									'actions' => array(),
350
								);
351
352
							if ($this->_uninstalling)
353
								$context['theme_actions'][$id]['actions'][] = array(
354
									'type' => $txt['package_delete'] . ' ' . ($action_data['type'] === 'require-dir' ? $txt['package_tree'] : $txt['package_file']),
355
									'action' => strtr($real_path, array('\\' => '/', BOARDDIR => '.')),
356
									'description' => '',
357
									'value' => base64_encode(json_encode(array('type' => $action_data['type'], 'orig' => $action_data['filename'], 'future' => $real_path, 'id' => $id))),
358
									'not_mod' => true,
359
								);
360
							else
361
								$context['theme_actions'][$id]['actions'][] = array(
362
									'type' => $txt['package_extract'] . ' ' . ($action_data['type'] === 'require-dir' ? $txt['package_tree'] : $txt['package_file']),
363
									'action' => strtr($real_path, array('\\' => '/', BOARDDIR => '.')),
364
									'description' => '',
365
									'value' => base64_encode(json_encode(array('type' => $action_data['type'], 'orig' => $action_data['destination'], 'future' => $real_path, 'id' => $id))),
366
									'not_mod' => true,
367
								);
368
						}
369
					}
370
				}
371
			}
372
		}
373
	}
374
375
	/**
376
	 * Extracts a package file in the packages/temp directory
377
	 *
378
	 * - Sets the base path as needed
379
	 * - Loads $this->_extracted_files with the package file listing
380
	 */
381
	private function _extract_files_temp()
382
	{
383
		// Is it a file in the package directory
384
		if (is_file(BOARDDIR . '/packages/' . $this->_filename))
385
		{
386
			// Unpack the files in to the packages/temp directory
387
			$this->_extracted_files = read_tgz_file(BOARDDIR . '/packages/' . $this->_filename, BOARDDIR . '/packages/temp');
388
389
			// Determine the base path for the package
390
			if ($this->_extracted_files && !file_exists(BOARDDIR . '/packages/temp/package-info.xml'))
391
			{
392
				foreach ($this->_extracted_files as $file)
0 ignored issues
show
Bug introduced by
The expression $this->_extracted_files of type boolean|array<integer,*> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
393
				{
394
					if (basename($file['filename']) === 'package-info.xml')
395
					{
396
						$this->_base_path = dirname($file['filename']) . '/';
397
						break;
398
					}
399
				}
400
			}
401
402
			if (!isset($this->_base_path))
403
				$this->_base_path = '';
404
		}
405
		// Perhaps its a directory then, assumed to be extracted
406
		elseif (!empty($this->_filename) && is_dir(BOARDDIR . '/packages/' . $this->_filename))
407
		{
408
			// Copy the directory to the temp directory
409
			copytree(BOARDDIR . '/packages/' . $this->_filename, BOARDDIR . '/packages/temp');
410
411
			// Get the file listing
412
			$this->_extracted_files = listtree(BOARDDIR . '/packages/temp');
413
			$this->_base_path = '';
414
		}
415
		// Well we don't know what it is then, so we stop
416
		else
417
			throw new \ElkArte\Exceptions\Exception('no_access', false);
418
	}
419
420
	/**
421
	 * Returns the actions that are required to install / uninstall / upgrade a package.
422
	 * Actions are defined by parsePackageInfo
423
	 * Sets the is_installed flag
424
	 *
425
	 * @param array   $package_installed
426
	 * @param array   $packageInfo Details for the package being tested/installed, set by getPackageInfo
427
	 * @param boolean $testing passed to parsePackageInfo, true for test install, false for real install
428
	 *
429
	 * @return array
430
	 * @throws \ElkArte\Exceptions\Exception package_cant_uninstall, package_uninstall_cannot
431
	 */
432
	private function _get_package_actions($package_installed, $packageInfo, $testing = true)
433
	{
434
		global $context;
435
436
		$actions = array();
437
438
		// Uninstalling?
439
		if ($this->_uninstalling)
440
		{
441
			// Wait, it's not installed yet!
442
			if (!isset($package_installed['old_version']))
443
			{
444
				deltree(BOARDDIR . '/packages/temp');
445
				throw new \ElkArte\Exceptions\Exception('package_cant_uninstall', false);
446
			}
447
448
			$actions = parsePackageInfo($packageInfo['xml'], $testing, 'uninstall');
449
450
			// Gadzooks!  There's no uninstaller at all!?
451
			if (empty($actions))
452
			{
453
				deltree(BOARDDIR . '/packages/temp');
454
				throw new \ElkArte\Exceptions\Exception('package_uninstall_cannot', false);
455
			}
456
457
			// Can't edit the custom themes it's edited if you're uninstalling, they must be removed.
458
			$context['themes_locked'] = true;
459
460
			// Only let them uninstall themes it was installed into.
461
			foreach ($this->theme_paths as $id => $data)
462
			{
463
				if ($id != 1 && !in_array($id, $package_installed['old_themes']))
464
					unset($this->theme_paths[$id]);
465
			}
466
		}
467
		// Or is it already installed and you want to upgrade
468
		elseif (isset($package_installed['old_version']) && $package_installed['old_version'] != $packageInfo['version'])
469
		{
470
			// Look for an upgrade...
471
			$actions = parsePackageInfo($packageInfo['xml'], $testing, 'upgrade', $package_installed['old_version']);
472
473
			// There was no upgrade....
474
			if (empty($actions))
475
				$this->_is_installed = true;
476
			else
477
			{
478
				// Otherwise they can only upgrade themes from the first time around.
479
				foreach ($this->theme_paths as $id => $data)
480
				{
481
					if ($id != 1 && !in_array($id, $package_installed['old_themes']))
482
						unset($this->theme_paths[$id]);
483
				}
484
			}
485
		}
486
		// Simply already installed
487
		elseif (isset($package_installed['old_version']) && $package_installed['old_version'] == $packageInfo['version'])
488
			$this->_is_installed = true;
489
490
		if (!isset($package_installed['old_version']) || $this->_is_installed)
491
			$actions = parsePackageInfo($packageInfo['xml'], $testing, 'install');
492
493
		return $actions;
494
	}
495
496
	/**
497
	 * Creates the packages temp directory
498
	 *
499
	 * - First trys as 755, failing moves to 777
500
	 * - Will try with FTP permissions for cases where the web server credentials
501
	 * do not have create directory permissions
502
	 */
503
	private function _create_temp_dir()
504
	{
505
		global $context, $scripturl;
506
507
		// Make the temp directory
508
		if (!mktree(BOARDDIR . '/packages/temp', 0755))
509
		{
510
			// 755 did not work, try 777?
511
			deltree(BOARDDIR . '/packages/temp', false);
512
			if (!mktree(BOARDDIR . '/packages/temp', 0777))
513
			{
514
				// That did not work either, we need additional permissions
515
				deltree(BOARDDIR . '/packages/temp', false);
516
				create_chmod_control(array(BOARDDIR . '/packages/temp/delme.tmp'), array('destination_url' => $scripturl . '?action=admin;area=packages;sa=' . $this->_req->query->sa . ';package=' . $context['filename'], 'crash_on_error' => true));
517
518
				// No temp directory was able to be made, that's fatal
519
				deltree(BOARDDIR . '/packages/temp', false);
520
				if (!mktree(BOARDDIR . '/packages/temp', 0777))
521
					throw new \ElkArte\Exceptions\Exception('package_cant_download', false);
522
			}
523
		}
524
	}
525
526
	/**
527
	 * Actually installs a package
528
	 */
529
	public function action_install2()
530
	{
531
		global $txt, $context, $scripturl, $modSettings;
532
533
		// Make sure we don't install this addon twice.
534
		checkSubmitOnce('check');
535
		checkSession();
536
537
		// If there's no package file, what are we installing?
538
		$this->_filename = $this->_req->getQuery('package', 'trim');
539
		if (empty($this->_filename))
540
			redirectexit('action=admin;area=packages');
541
542
		// And if the file does not exist there is a problem
543 View Code Duplication
		if (!file_exists(BOARDDIR . '/packages/' . $this->_filename))
544
			throw new \ElkArte\Exceptions\Exception('package_no_file', false);
545
546
		// If this is an uninstall, we'll have an id.
547
		$this->install_id = $this->_req->getQuery('pid', 'intval', 0);
548
549
		// Installing in themes will require some help
550
		require_once(SUBSDIR . '/Themes.subs.php');
551
552
		// @todo Perhaps do it in steps, if necessary?
553
		$this->_uninstalling = $this->_req->query->sa === 'uninstall2';
554
555
		// Load up the package FTP information?
556
		create_chmod_control(array(), array('destination_url' => $scripturl . '?action=admin;area=packages;sa=' . $this->_req->query->sa . ';package=' . $this->_req->query->package));
557
558
		// Make sure temp directory exists and is empty!
559 View Code Duplication
		if (file_exists(BOARDDIR . '/packages/temp'))
560
			deltree(BOARDDIR . '/packages/temp', false);
561
		else
562
			$this->_create_temp_dir();
563
564
		// Let the unpacker do the work.
565
		$this->_extract_files_temp();
566
567
		// Are we installing this into any custom themes?
568
		$custom_themes = array(1);
569
		$known_themes = explode(',', $modSettings['knownThemes']);
570
		if (!empty($this->_req->post->custom_theme))
571
		{
572
			foreach ($this->_req->post->custom_theme as $tid)
573
				if (in_array($tid, $known_themes))
574
					$custom_themes[] = (int) $tid;
575
		}
576
577
		// Now load up the paths of the themes that we need to know about.
578
		$this->theme_paths = getThemesPathbyID($custom_themes);
579
		$themes_installed = array(1);
580
581
		// Are there any theme copying that we want to take place?
582
		$context['theme_copies'] = array(
583
			'require-file' => array(),
584
			'require-dir' => array(),
585
		);
586
587
		if (!empty($this->_req->post->theme_changes))
588
		{
589
			foreach ($this->_req->post->theme_changes as $change)
590
			{
591
				if (empty($change))
592
					continue;
593
594
				$theme_data = json_decode(base64_decode($change), true);
595
				if (empty($theme_data['type']))
596
					continue;
597
598
				$themes_installed[] = $theme_data['id'];
599
				$context['theme_copies'][$theme_data['type']][$theme_data['orig']][] = $theme_data['future'];
600
			}
601
		}
602
603
		// Get the package info...
604
		$packageInfo = getPackageInfo($this->_filename);
605
		if (!is_array($packageInfo))
606
			throw new \ElkArte\Exceptions\Exception($packageInfo);
607
608
		$packageInfo['filename'] = $this->_filename;
609
610
		$context['base_path'] = $this->_base_path;
611
612
		// Create a backup file to roll back to! (but if they do this more than once, don't run it a zillion times.)
613
		if (!empty($modSettings['package_make_full_backups']) && (!isset($_SESSION['last_backup_for']) || $_SESSION['last_backup_for'] != $this->_filename . ($this->_uninstalling ? '$$' : '$')))
614
		{
615
			$_SESSION['last_backup_for'] = $this->_filename . ($this->_uninstalling ? '$$' : '$');
616
617
			// @todo Internationalize this?
618
			package_create_backup(($this->_uninstalling ? 'backup_' : 'before_') . strtok($this->_filename, '.'));
619
		}
620
621
		// The addon isn't installed.... unless proven otherwise.
622
		$this->_is_installed = false;
623
624
		// Is it actually installed?
625
		$package_installed = isPackageInstalled($packageInfo['id'], $this->install_id);
626
627
		// Fetch the install status and action log
628
		$install_log = $this->_get_package_actions($package_installed, $packageInfo, false);
629
630
		// Set up the details for the sub template, linktree, etc
631
		$context['linktree'][count($context['linktree']) - 1] = array(
632
			'url' => $scripturl . '?action=admin;area=packages;sa=browse',
633
			'name' => $this->_uninstalling ? $txt['uninstall'] : $txt['extracting']
634
		);
635
		$context['page_title'] .= ' - ' . ($this->_uninstalling ? $txt['uninstall'] : $txt['extracting']);
636
		$context['sub_template'] = 'extract_package';
637
		$context['filename'] = $this->_filename;
638
		$context['install_finished'] = false;
639
		$context['is_installed'] = $this->_is_installed;
640
		$context['uninstalling'] = $this->_uninstalling;
641
		$context['extract_type'] = isset($packageInfo['type']) ? $packageInfo['type'] : 'modification';
642
643
		// We're gonna be needing the table db functions! ...Sometimes.
644
		$table_installer = db_table();
645
646
		// @todo Make a log of any errors that occurred and output them?
647
		if (!empty($install_log))
648
		{
649
			// @todo Make a log of any errors that occurred and output them?
650
			$pka = new PackageActions(new \ElkArte\EventManager());
651
			$pka->setUser(\ElkArte\User::$info);
652
			$pka->install_init($install_log, $this->_uninstalling, $this->_base_path, $this->theme_paths, $themes_installed);
653
			$failed_steps = $pka->failed_steps;
654
			$themes_installed = $pka->themes_installed;
655
656
			package_flush_cache();
657
658
			// First, ensure this change doesn't get removed by putting a stake in the ground (So to speak).
659
			package_put_contents(BOARDDIR . '/packages/installed.list', time());
660
661
			// See if this is already installed
662
			$is_upgrade = false;
663
			$old_db_changes = array();
664
			$package_check = isPackageInstalled($packageInfo['id']);
665
666
			// Change the installed state as required.
667
			if (!empty($package_check['install_state']))
668
			{
669
				if ($this->_uninstalling)
670
					setPackageState($package_check['package_id'], $this->install_id);
671
				else
672
				{
673
					// not uninstalling so must be an upgrade
674
					$is_upgrade = true;
675
					$old_db_changes = empty($package_check['db_changes']) ? array() : $package_check['db_changes'];
676
				}
677
			}
678
679
			// Assuming we're not uninstalling, add the entry.
680
			if (!$this->_uninstalling)
681
			{
682
				// Any db changes from older version?
683
				$table_log = $table_installer->package_log();
684
685
				if (!empty($old_db_changes))
686
					$db_package_log = empty($table_log) ? $old_db_changes : array_merge($old_db_changes, $table_log);
687
				else
688
					$db_package_log = $table_log;
689
690
				// If there are some database changes we might want to remove then filter them out.
691
				if (!empty($db_package_log))
692
				{
693
					// We're really just checking for entries which are create table AND add columns (etc).
694
					$tables = array();
695
					usort($db_package_log, array($this, '_sort_table_first'));
696
					foreach ($db_package_log as $k => $log)
697
					{
698
						if ($log[0] === 'remove_table')
699
							$tables[] = $log[1];
700
						elseif (in_array($log[1], $tables))
701
							unset($db_package_log[$k]);
702
					}
703
704
					$package_installed['db_changes'] = serialize($db_package_log);
705
				}
706
				else
707
					$package_installed['db_changes'] = '';
708
709
				// What themes did we actually install?
710
				$themes_installed = array_unique($themes_installed);
711
				$themes_installed = implode(',', $themes_installed);
712
713
				// What failed steps?
714
				$failed_step_insert = serialize($failed_steps);
715
716
				// Credits tag?
717
				$credits_tag = (empty($pka->credits_tag)) ? '' : serialize($pka->credits_tag);
718
719
				// Add to the log packages
720
				addPackageLog($packageInfo, $failed_step_insert, $themes_installed, $package_installed['db_changes'], $is_upgrade, $credits_tag);
721
			}
722
723
			$context['install_finished'] = true;
724
		}
725
726
		// If there's database changes - and they want them removed - let's do it last!
727
		if (!empty($package_installed['db_changes']) && !empty($this->_req->post->do_db_changes))
728
		{
729
			foreach ($package_installed['db_changes'] as $change)
730
			{
731
				if ($change[0] === 'remove_table' && isset($change[1]))
732
					$table_installer->drop_table($change[1]);
733
				elseif ($change[0] === 'remove_column' && isset($change[2]))
734
					$table_installer->remove_column($change[1], $change[2]);
735
				elseif ($change[0] === 'remove_index' && isset($change[2]))
736
					$table_installer->remove_index($change[1], $change[2]);
737
			}
738
		}
739
740
		// Clean house... get rid of the evidence ;).
741
		if (file_exists(BOARDDIR . '/packages/temp'))
742
			deltree(BOARDDIR . '/packages/temp');
743
744
		// Log what we just did.
745
		logAction($this->_uninstalling ? 'uninstall_package' : (!empty($is_upgrade) ? 'upgrade_package' : 'install_package'), array('package' => \ElkArte\Util::htmlspecialchars($packageInfo['name']), 'version' => \ElkArte\Util::htmlspecialchars($packageInfo['version'])), 'admin');
746
747
		// Just in case, let's clear the whole cache to avoid anything going up the swanny.
748
		\ElkArte\Cache\Cache::instance()->clean();
749
750
		// Restore file permissions?
751
		create_chmod_control(array(), array(), true);
752
	}
753
754
	/**
755
	 * Table sorting function used in usort
756
	 *
757
	 * @param string[] $a
758
	 * @param string[] $b
759
	 *
760
	 * @return int
761
	 */
762
	private function _sort_table_first($a, $b)
763
	{
764
		if ($a[0] == $b[0])
765
			return 0;
766
767
		return $a[0] === 'remove_table' ? -1 : 1;
768
	}
769
770
	/**
771
	 * List the files in a package.
772
	 */
773
	public function action_list()
774
	{
775
		global $txt, $scripturl, $context;
776
777
		// No package?  Show him or her the door.
778 View Code Duplication
		if (!isset($this->_req->query->package) || $this->_req->query->package == '')
779
			redirectexit('action=admin;area=packages');
780
781
		$context['linktree'][] = array(
782
			'url' => $scripturl . '?action=admin;area=packages;sa=list;package=' . $this->_req->query->package,
783
			'name' => $txt['list_file']
784
		);
785
		$context['page_title'] .= ' - ' . $txt['list_file'];
786
		$context['sub_template'] = 'list';
787
788
		// The filename...
789
		$context['filename'] = $this->_req->query->package;
790
791
		// Let the unpacker do the work.
792
		if (is_file(BOARDDIR . '/packages/' . $context['filename']))
793
			$context['files'] = read_tgz_file(BOARDDIR . '/packages/' . $context['filename'], null);
794
		elseif (is_dir(BOARDDIR . '/packages/' . $context['filename']))
795
			$context['files'] = listtree(BOARDDIR . '/packages/' . $context['filename']);
796
	}
797
798
	/**
799
	 * Display one of the files in a package.
800
	 */
801
	public function action_examine()
802
	{
803
		global $txt, $scripturl, $context;
804
805
		// No package?  Show him or her the door.
806 View Code Duplication
		if (!isset($this->_req->query->package) || $this->_req->query->package == '')
807
			redirectexit('action=admin;area=packages');
808
809
		// No file?  Show him or her the door.
810 View Code Duplication
		if (!isset($this->_req->query->file) || $this->_req->query->file == '')
811
			redirectexit('action=admin;area=packages');
812
813
		$this->_req->query->package = preg_replace('~[\.]+~', '.', strtr($this->_req->query->package, array('/' => '_', '\\' => '_')));
814
		$this->_req->query->file = preg_replace('~[\.]+~', '.', $this->_req->query->file);
815
816
		if (isset($this->_req->query->raw))
817
		{
818
			if (is_file(BOARDDIR . '/packages/' . $this->_req->query->package))
819
				echo read_tgz_file(BOARDDIR . '/packages/' . $this->_req->query->package, $this->_req->query->file, true);
820 View Code Duplication
			elseif (is_dir(BOARDDIR . '/packages/' . $this->_req->query->package))
821
				echo file_get_contents(BOARDDIR . '/packages/' . $this->_req->query->package . '/' . $this->_req->query->file);
822
823
			obExit(false);
824
		}
825
826
		$context['linktree'][count($context['linktree']) - 1] = array(
827
			'url' => $scripturl . '?action=admin;area=packages;sa=list;package=' . $this->_req->query->package,
828
			'name' => $txt['package_examine_file']
829
		);
830
		$context['page_title'] .= ' - ' . $txt['package_examine_file'];
831
		$context['sub_template'] = 'examine';
832
833
		// The filename...
834
		$context['package'] = $this->_req->query->package;
835
		$context['filename'] = $this->_req->query->file;
836
837
		// Let the unpacker do the work.... but make sure we handle images properly.
838
		if (in_array(strtolower(strrchr($this->_req->query->file, '.')), array('.bmp', '.gif', '.jpeg', '.jpg', '.png')))
839
			$context['filedata'] = '<img src="' . $scripturl . '?action=admin;area=packages;sa=examine;package=' . $this->_req->query->package . ';file=' . $this->_req->query->file . ';raw" alt="' . $this->_req->query->file . '" />';
840
		else
841
		{
842
			if (is_file(BOARDDIR . '/packages/' . $this->_req->query->package))
843
				$context['filedata'] = htmlspecialchars(read_tgz_file(BOARDDIR . '/packages/' . $this->_req->query->package, $this->_req->query->file, true));
844 View Code Duplication
			elseif (is_dir(BOARDDIR . '/packages/' . $this->_req->query->package))
845
				$context['filedata'] = htmlspecialchars(file_get_contents(BOARDDIR . '/packages/' . $this->_req->query->package . '/' . $this->_req->query->file));
846
		}
847
	}
848
849
	/**
850
	 * Empty out the installed list.
851
	 */
852
	public function action_flush()
853
	{
854
		// Always check the session.
855
		checkSession('get');
856
857
		include_once(SUBSDIR . '/Package.subs.php');
858
859
		// Record when we last did this.
860
		package_put_contents(BOARDDIR . '/packages/installed.list', time());
861
862
		// Set everything as uninstalled.
863
		setPackagesAsUninstalled();
864
865
		redirectexit('action=admin;area=packages;sa=installed');
866
	}
867
868
	/**
869
	 * Delete a package.
870
	 */
871
	public function action_remove()
872
	{
873
		global $scripturl;
874
875
		// Check it.
876
		checkSession('get');
877
878
		// Ack, don't allow deletion of arbitrary files here, could become a security hole somehow!
879
		if (!isset($this->_req->query->package) || $this->_req->query->package === 'index.php' || $this->_req->query->package === 'installed.list' || $this->_req->query->package === 'backups')
880
			redirectexit('action=admin;area=packages;sa=browse');
881
		$this->_req->query->package = preg_replace('~[\.]+~', '.', strtr($this->_req->query->package, array('/' => '_', '\\' => '_')));
882
883
		// Can't delete what's not there.
884
		if (file_exists(BOARDDIR . '/packages/' . $this->_req->query->package)
885
			&& (substr($this->_req->query->package, -4) === '.zip' || substr($this->_req->query->package, -4) === '.tgz' || substr($this->_req->query->package, -7) === '.tar.gz' || is_dir(BOARDDIR . '/packages/' . $this->_req->query->package))
886
			&& $this->_req->query->package !== 'backups' && substr($this->_req->query->package, 0, 1) !== '.')
887
		{
888
			create_chmod_control(array(BOARDDIR . '/packages/' . $this->_req->query->package), array('destination_url' => $scripturl . '?action=admin;area=packages;sa=remove;package=' . $this->_req->query->package, 'crash_on_error' => true));
889
890
			if (is_dir(BOARDDIR . '/packages/' . $this->_req->query->package))
891
				deltree(BOARDDIR . '/packages/' . $this->_req->query->package);
892
			else
893
			{
894
				@chmod(BOARDDIR . '/packages/' . $this->_req->query->package, 0777);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
895
				unlink(BOARDDIR . '/packages/' . $this->_req->query->package);
896
			}
897
		}
898
899
		redirectexit('action=admin;area=packages;sa=browse');
900
	}
901
902
	/**
903
	 * Browse a list of installed packages.
904
	 */
905
	public function action_browse()
906
	{
907
		global $txt, $scripturl, $context;
908
909
		$context['page_title'] .= ' - ' . $txt['browse_packages'];
910
		$context['forum_version'] = FORUM_VERSION;
911
		$context['available_addon'] = array();
912
		$context['available_avatar'] = array();
913
		$context['available_smiley'] = array();
914
		$context['available_language'] = array();
915
		$context['available_unknown'] = array();
916
917
		$installed = $context['sub_action'] === 'installed' ? true : false;
918
		$context['package_types'] = $installed ? array('addon') : array('addon', 'avatar', 'language', 'smiley', 'unknown');
919
920
		foreach ($context['package_types'] as $type)
921
		{
922
			// Use the standard templates for showing this.
923
			$listOptions = array(
924
				'id' => 'packages_lists_' . $type,
925
				'title' => $installed ? $txt['view_and_remove'] : $txt[($type === 'addon' ? 'modification' : $type) . '_package'],
926
				'no_items_label' => $txt['no_packages'],
927
				'get_items' => array(
928
					'function' => array($this, 'list_packages'),
929
					'params' => array('type' => $type, 'installed' => $installed),
930
				),
931
				'base_href' => $scripturl . '?action=admin;area=packages;sa=' . $context['sub_action'] . ';type=' . $type,
932
				'default_sort_col' => 'mod_name' . $type,
933
				'columns' => array(
934
					'mod_name' . $type => array(
935
						'header' => array(
936
							'value' => $txt['mod_name'],
937
							'style' => 'width: 25%;',
938
						),
939
						'data' => array(
940 View Code Duplication
							'function' => function ($package_md5) use ($type)  {
941
								global $context;
942
943
								if (isset($context['available_' . $type . ''][$package_md5]))
944
									return $context['available_' . $type . ''][$package_md5]['name'];
945
946
								return '';
947
							},
948
						),
949
						'sort' => array(
950
							'default' => 'name',
951
							'reverse' => 'name',
952
						),
953
					),
954
					'version' . $type => array(
955
						'header' => array(
956
							'value' => $txt['mod_version'],
957
							'style' => 'width: 25%;',
958
						),
959
						'data' => array(
960 View Code Duplication
							'function' => function ($package_md5) use ($type)  {
961
								global $context;
962
963
								if (isset($context['available_' . $type . ''][$package_md5]))
964
									return $context['available_' . $type . ''][$package_md5]['version'];
965
966
								return '';
967
							},
968
						),
969
						'sort' => array(
970
							'default' => 'version',
971
							'reverse' => 'version',
972
						),
973
					),
974
					'operations' . $type => array(
975
						'header' => array(
976
							'value' => '',
977
						),
978
						'data' => array(
979
							'function' => function ($package_md5) use ($type) {
980
								global $context, $scripturl, $txt;
981
982
								if (!isset($context['available_' . $type . ''][$package_md5]))
983
									return '';
984
985
								// Rewrite shortcut
986
								$package = $context['available_' . $type . ''][$package_md5];
987
								$return = '';
988
989
								if ($package['can_uninstall'])
990
									$return = '
991
										<a class="linkbutton" href="' . $scripturl . '?action=admin;area=packages;sa=uninstall;package=' . $package['filename'] . ';pid=' . $package['installed_id'] . '">' . $txt['uninstall'] . '</a>';
992
								elseif ($package['can_emulate_uninstall'])
993
									$return = '
994
										<a class="linkbutton" href="' . $scripturl . '?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>';
995 View Code Duplication
								elseif ($package['can_upgrade'])
996
									$return = '
997
										<a class="linkbutton" href="' . $scripturl . '?action=admin;area=packages;sa=install;package=' . $package['filename'] . '">' . $txt['package_upgrade'] . '</a>';
998 View Code Duplication
								elseif ($package['can_install'])
999
									$return = '
1000
										<a class="linkbutton" href="' . $scripturl . '?action=admin;area=packages;sa=install;package=' . $package['filename'] . '">' . $txt['install_mod'] . '</a>';
1001
								elseif ($package['can_emulate_install'])
1002
									$return = '
1003
										<a class="linkbutton" href="' . $scripturl . '?action=admin;area=packages;sa=install;ve=' . $package['can_emulate_install'] . ';package=' . $package['filename'] . '">' . $txt['package_emulate_install'] . ' ' . $package['can_emulate_install'] . '</a>';
1004
1005
								return $return . '
1006
										<a class="linkbutton" href="' . $scripturl . '?action=admin;area=packages;sa=list;package=' . $package['filename'] . '">' . $txt['list_files'] . '</a>
1007
										<a class="linkbutton" href="' . $scripturl . '?action=admin;area=packages;sa=remove;package=' . $package['filename'] . ';' . $context['session_var'] . '=' . $context['session_id'] . '"' . ($package['is_installed'] && $package['is_current']
1008
											? ' onclick="return confirm(\'' . $txt['package_delete_bad'] . '\');"'
1009
											: '') . '>' . $txt['package_delete'] . '</a>';
1010
							},
1011
							'class' => 'righttext',
1012
						),
1013
					),
1014
				),
1015
				'additional_rows' => array(
1016
					array(
1017
						'position' => 'bottom_of_list',
1018
						'class' => 'submitbutton',
1019
						'value' => ($context['sub_action'] === 'browse'
1020
							? '<div class="smalltext">' . $txt['package_installed_key'] . '<i class="icon icon-small i-green-dot"></i>' . $txt['package_installed_current'] . '<i class="icon icon-small i-red-dot"></i>' . $txt['package_installed_old'] . '</div>'
1021
							: '<a class="linkbutton" href="' . $scripturl . '?action=admin;area=packages;sa=flush;' . $context['session_var'] . '=' . $context['session_id'] . '" onclick="return confirm(\'' . $txt['package_delete_list_warning'] . '\');">' . $txt['delete_list'] . '</a>'),
1022
					),
1023
				),
1024
			);
1025
1026
			createList($listOptions);
1027
		}
1028
1029
		$context['sub_template'] = 'browse';
1030
		$context['default_list'] = 'packages_lists';
1031
	}
1032
1033
	/**
1034
	 * Test an FTP connection.
1035
	 *
1036
	 * @uses Xml Template, generic_xml sub template
1037
	 */
1038
	public function action_ftptest()
1039
	{
1040
		global $context, $txt, $package_ftp;
1041
1042
		checkSession('get');
1043
1044
		// Try to make the FTP connection.
1045
		create_chmod_control(array(), array('force_find_error' => true));
1046
1047
		// Deal with the template stuff.
1048
		theme()->getTemplates()->load('Xml');
1049
		$context['sub_template'] = 'generic_xml';
1050
		theme()->getLayers()->removeAll();
1051
1052
		// Define the return data, this is simple.
1053
		$context['xml_data'] = array(
1054
			'results' => array(
1055
				'identifier' => 'result',
1056
				'children' => array(
1057
					array(
1058
						'attributes' => array(
1059
							'success' => !empty($package_ftp) ? 1 : 0,
1060
						),
1061
						'value' => !empty($package_ftp) ? $txt['package_ftp_test_success'] : (isset($context['package_ftp'], $context['package_ftp']['error']) ? $context['package_ftp']['error'] : $txt['package_ftp_test_failed']),
1062
					),
1063
				),
1064
			),
1065
		);
1066
	}
1067
1068
	/**
1069
	 * Used when a temp FTP access is needed to package functions
1070
	 */
1071
	public function action_options()
1072
	{
1073
		global $txt, $context, $modSettings;
1074
1075
		if (isset($this->_req->post->save))
1076
		{
1077
			checkSession('post');
1078
1079
			updateSettings(array(
1080
				'package_server' => $this->_req->getPost('pack_server', 'trim|\\ElkArte\\Util::htmlspecialchars'),
1081
				'package_port' => $this->_req->getPost('pack_port', 'trim|\\ElkArte\\Util::htmlspecialchars'),
1082
				'package_username' => $this->_req->getPost('pack_user', 'trim|\\ElkArte\\Util::htmlspecialchars'),
1083
				'package_make_backups' => !empty($this->_req->post->package_make_backups),
1084
				'package_make_full_backups' => !empty($this->_req->post->package_make_full_backups)
1085
			));
1086
1087
			redirectexit('action=admin;area=packages;sa=options');
1088
		}
1089
1090
		if (preg_match('~^/home\d*/([^/]+?)/public_html~', $this->_req->server->DOCUMENT_ROOT, $match))
1091
			$default_username = $match[1];
1092
		else
1093
			$default_username = '';
1094
1095
		$context['page_title'] = $txt['package_settings'];
1096
		$context['sub_template'] = 'install_options';
1097
		$context['package_ftp_server'] = isset($modSettings['package_server']) ? $modSettings['package_server'] : 'localhost';
1098
		$context['package_ftp_port'] = isset($modSettings['package_port']) ? $modSettings['package_port'] : '21';
1099
		$context['package_ftp_username'] = isset($modSettings['package_username']) ? $modSettings['package_username'] : $default_username;
1100
		$context['package_make_backups'] = !empty($modSettings['package_make_backups']);
1101
		$context['package_make_full_backups'] = !empty($modSettings['package_make_full_backups']);
1102
	}
1103
1104
	/**
1105
	 * List operations
1106
	 */
1107
	public function action_showoperations()
1108
	{
1109
		global $context, $txt;
1110
1111
		// Can't be in here buddy.
1112
		isAllowedTo('admin_forum');
1113
1114
		// We need to know the operation key for the search and replace?
1115
		if (!isset($this->_req->query->operation_key, $this->_req->query->filename) && !is_numeric($this->_req->query->operation_key))
1116
			throw new \ElkArte\Exceptions\Exception('operation_invalid', 'general');
1117
1118
		// Load the required file.
1119
		require_once(SUBSDIR . '/Themes.subs.php');
1120
1121
		// Uninstalling the mod?
1122
		$reverse = isset($this->_req->query->reverse) ? true : false;
1123
1124
		// Get the base name.
1125
		$context['filename'] = preg_replace('~[\.]+~', '.', $this->_req->query->package);
1126
1127
		// We need to extract this again.
1128
		if (is_file(BOARDDIR . '/packages/' . $context['filename']))
1129
		{
1130
			$context['extracted_files'] = read_tgz_file(BOARDDIR . '/packages/' . $context['filename'], BOARDDIR . '/packages/temp');
1131
			if ($context['extracted_files'] && !file_exists(BOARDDIR . '/packages/temp/package-info.xml'))
1132
			{
1133 View Code Duplication
				foreach ($context['extracted_files'] as $file)
0 ignored issues
show
Bug introduced by
The expression $context['extracted_files'] of type boolean|array<integer,*> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1134
				{
1135
					if (basename($file['filename']) === 'package-info.xml')
1136
					{
1137
						$context['base_path'] = dirname($file['filename']) . '/';
1138
						break;
1139
					}
1140
				}
1141
			}
1142
1143
			if (!isset($context['base_path']))
1144
				$context['base_path'] = '';
1145
		}
1146
		elseif (is_dir(BOARDDIR . '/packages/' . $context['filename']))
1147
		{
1148
			copytree(BOARDDIR . '/packages/' . $context['filename'], BOARDDIR . '/packages/temp');
1149
			$context['extracted_files'] = listtree(BOARDDIR . '/packages/temp');
1150
			$context['base_path'] = '';
1151
		}
1152
1153
		// Load up any custom themes we may want to install into...
1154
		$theme_paths = getThemesPathbyID();
1155
1156
		// For uninstall operations we only consider the themes in which the package is installed.
1157
		if ($reverse && !empty($this->_req->query->install_id))
1158
		{
1159
			$install_id = (int) $this->_req->query->install_id;
1160
			if ($install_id > 0)
1161
			{
1162
				$old_themes = loadThemesAffected($install_id);
1163
				foreach ($theme_paths as $id => $data)
1164
				{
1165
					if ($id != 1 && !in_array($id, $old_themes))
1166
						unset($theme_paths[$id]);
1167
				}
1168
			}
1169
		}
1170
1171
		$mod_actions = parseModification(@file_get_contents(BOARDDIR . '/packages/temp/' . $context['base_path'] . $this->_req->query->filename), true, $reverse, $theme_paths);
1172
1173
		// Ok lets get the content of the file.
1174
		$context['operations'] = array(
1175
			'search' => strtr(htmlspecialchars($mod_actions[$this->_req->query->operation_key]['search_original'], ENT_COMPAT, 'UTF-8'), array('[' => '&#91;', ']' => '&#93;')),
1176
			'replace' => strtr(htmlspecialchars($mod_actions[$this->_req->query->operation_key]['replace_original'], ENT_COMPAT, 'UTF-8'), array('[' => '&#91;', ']' => '&#93;')),
1177
			'position' => $mod_actions[$this->_req->query->operation_key]['position'],
1178
		);
1179
1180
		// Let's do some formatting...
1181
		$operation_text = $context['operations']['position'] === 'replace' ? 'operation_replace' : ($context['operations']['position'] === 'before' ? 'operation_after' : 'operation_before');
1182
		$bbc_parser = \BBC\ParserWrapper::instance();
1183
		$context['operations']['search'] = $bbc_parser->parsePackage('[code=' . $txt['operation_find'] . ']' . ($context['operations']['position'] === 'end' ? '?&gt;' : $context['operations']['search']) . '[/code]');
1184
		$context['operations']['replace'] = $bbc_parser->parsePackage('[code=' . $txt[$operation_text] . ']' . $context['operations']['replace'] . '[/code]');
1185
1186
		// No layers
1187
		theme()->getLayers()->removeAll();
1188
		$context['sub_template'] = 'view_operations';
1189
	}
1190
1191
	/**
1192
	 * Allow the admin to reset permissions on files.
1193
	 */
1194
	public function action_perms()
1195
	{
1196
		global $context, $txt, $modSettings, $package_ftp;
1197
1198
		// Let's try and be good, yes?
1199
		checkSession('get');
1200
1201
		// If we're restoring permissions this is just a pass through really.
1202
		if (isset($this->_req->query->restore))
1203
		{
1204
			create_chmod_control(array(), array(), true);
1205
			throw new \ElkArte\Exceptions\Exception('no_access', false);
1206
		}
1207
1208
		// This is a time and memory eating ...
1209
		detectServer()->setMemoryLimit('128M');
1210
		detectServer()->setTimeLimit(600);
1211
1212
		// Load up some FTP stuff.
1213
		create_chmod_control();
1214
1215
		if (empty($package_ftp) && !isset($this->_req->post->skip_ftp))
1216
		{
1217
			$ftp = new \ElkArte\Http\FtpConnection(null);
1218
			list ($username, $detect_path) = $ftp->detect_path(BOARDDIR);
1219
1220
			$context['package_ftp'] = array(
1221
				'server' => isset($modSettings['package_server']) ? $modSettings['package_server'] : 'localhost',
1222
				'port' => isset($modSettings['package_port']) ? $modSettings['package_port'] : '21',
1223
				'username' => empty($username) ? (isset($modSettings['package_username']) ? $modSettings['package_username'] : '') : $username,
1224
				'path' => $detect_path,
1225
				'form_elements_only' => true,
1226
			);
1227
		}
1228
		else
1229
			$context['ftp_connected'] = true;
1230
1231
		// Define the template.
1232
		$context['page_title'] = $txt['package_file_perms'];
1233
		$context['sub_template'] = 'file_permissions';
1234
1235
		// Define what files we're interested in, as a tree.
1236
		$context['file_tree'] = array(
1237
			strtr(BOARDDIR, array('\\' => '/')) => array(
1238
				'type' => 'dir',
1239
				'contents' => array(
1240
					'agreement.txt' => array(
1241
						'type' => 'file',
1242
						'writable_on' => 'standard',
1243
					),
1244
					'Settings.php' => array(
1245
						'type' => 'file',
1246
						'writable_on' => 'restrictive',
1247
					),
1248
					'Settings_bak.php' => array(
1249
						'type' => 'file',
1250
						'writable_on' => 'restrictive',
1251
					),
1252
					'attachments' => array(
1253
						'type' => 'dir',
1254
						'writable_on' => 'restrictive',
1255
					),
1256
					'avatars' => array(
1257
						'type' => 'dir_recursive',
1258
						'writable_on' => 'standard',
1259
					),
1260
					'cache' => array(
1261
						'type' => 'dir',
1262
						'writable_on' => 'restrictive',
1263
					),
1264
					'custom_avatar_dir' => array(
1265
						'type' => 'dir',
1266
						'writable_on' => 'restrictive',
1267
					),
1268
					'smileys' => array(
1269
						'type' => 'dir_recursive',
1270
						'writable_on' => 'standard',
1271
					),
1272
					'sources' => array(
1273
						'type' => 'dir_recursive',
1274
						'list_contents' => true,
1275
						'writable_on' => 'standard',
1276
						'contents' => array(
1277
							'database' => array(
1278
								'type' => 'dir',
1279
								'list_contents' => true,
1280
							),
1281
							'ElkArte' => array(
1282
								'type' => 'dir_recursive',
1283
								'list_contents' => true,
1284
							),
1285
							'ext' => array(
1286
								'type' => 'dir',
1287
								'list_contents' => true,
1288
							),
1289
							'subs' => array(
1290
								'type' => 'dir',
1291
								'list_contents' => true,
1292
							),
1293
						),
1294
					),
1295
					'themes' => array(
1296
						'type' => 'dir_recursive',
1297
						'writable_on' => 'standard',
1298
						'contents' => array(
1299
							'default' => array(
1300
								'type' => 'dir_recursive',
1301
								'list_contents' => true,
1302
								'contents' => array(
1303
									'languages' => array(
1304
										'type' => 'dir',
1305
										'list_contents' => true,
1306
									),
1307
								),
1308
							),
1309
						),
1310
					),
1311
					'packages' => array(
1312
						'type' => 'dir',
1313
						'writable_on' => 'standard',
1314
						'contents' => array(
1315
							'temp' => array(
1316
								'type' => 'dir',
1317
							),
1318
							'backup' => array(
1319
								'type' => 'dir',
1320
							),
1321
							'installed.list' => array(
1322
								'type' => 'file',
1323
								'writable_on' => 'standard',
1324
							),
1325
						),
1326
					),
1327
				),
1328
			),
1329
		);
1330
1331
		// Directories that can move.
1332 View Code Duplication
		if (substr(SOURCEDIR, 0, strlen(BOARDDIR)) != BOARDDIR)
1333
		{
1334
			unset($context['file_tree'][strtr(BOARDDIR, array('\\' => '/'))]['contents']['sources']);
1335
			$context['file_tree'][strtr(SOURCEDIR, array('\\' => '/'))] = array(
1336
				'type' => 'dir',
1337
				'list_contents' => true,
1338
				'writable_on' => 'standard',
1339
			);
1340
		}
1341
1342
		// Moved the cache?
1343 View Code Duplication
		if (substr(CACHEDIR, 0, strlen(BOARDDIR)) != BOARDDIR)
1344
		{
1345
			unset($context['file_tree'][strtr(BOARDDIR, array('\\' => '/'))]['contents']['cache']);
1346
			$context['file_tree'][strtr(CACHEDIR, array('\\' => '/'))] = array(
1347
				'type' => 'dir',
1348
				'list_contents' => false,
1349
				'writable_on' => 'restrictive',
1350
			);
1351
		}
1352
1353
		// Are we using multiple attachment directories?
1354
		if (!empty($modSettings['currentAttachmentUploadDir']))
1355
		{
1356
			unset($context['file_tree'][strtr(BOARDDIR, array('\\' => '/'))]['contents']['attachments']);
1357
1358
			if (!is_array($modSettings['attachmentUploadDir']))
1359
				$modSettings['attachmentUploadDir'] = \ElkArte\Util::unserialize($modSettings['attachmentUploadDir']);
1360
1361
			// @todo Should we suggest non-current directories be read only?
1362
			foreach ($modSettings['attachmentUploadDir'] as $dir)
1363
				$context['file_tree'][strtr($dir, array('\\' => '/'))] = array(
1364
					'type' => 'dir',
1365
					'writable_on' => 'restrictive',
1366
				);
1367
		}
1368 View Code Duplication
		elseif (substr($modSettings['attachmentUploadDir'], 0, strlen(BOARDDIR)) != BOARDDIR)
1369
		{
1370
			unset($context['file_tree'][strtr(BOARDDIR, array('\\' => '/'))]['contents']['attachments']);
1371
			$context['file_tree'][strtr($modSettings['attachmentUploadDir'], array('\\' => '/'))] = array(
1372
				'type' => 'dir',
1373
				'writable_on' => 'restrictive',
1374
			);
1375
		}
1376
1377 View Code Duplication
		if (substr($modSettings['smileys_dir'], 0, strlen(BOARDDIR)) != BOARDDIR)
1378
		{
1379
			unset($context['file_tree'][strtr(BOARDDIR, array('\\' => '/'))]['contents']['smileys']);
1380
			$context['file_tree'][strtr($modSettings['smileys_dir'], array('\\' => '/'))] = array(
1381
				'type' => 'dir_recursive',
1382
				'writable_on' => 'standard',
1383
			);
1384
		}
1385
1386 View Code Duplication
		if (substr($modSettings['avatar_directory'], 0, strlen(BOARDDIR)) != BOARDDIR)
1387
		{
1388
			unset($context['file_tree'][strtr(BOARDDIR, array('\\' => '/'))]['contents']['avatars']);
1389
			$context['file_tree'][strtr($modSettings['avatar_directory'], array('\\' => '/'))] = array(
1390
				'type' => 'dir',
1391
				'writable_on' => 'standard',
1392
			);
1393
		}
1394
1395 View Code Duplication
		if (isset($modSettings['custom_avatar_dir']) && substr($modSettings['custom_avatar_dir'], 0, strlen(BOARDDIR)) != BOARDDIR)
1396
		{
1397
			unset($context['file_tree'][strtr(BOARDDIR, array('\\' => '/'))]['contents']['custom_avatar_dir']);
1398
			$context['file_tree'][strtr($modSettings['custom_avatar_dir'], array('\\' => '/'))] = array(
1399
				'type' => 'dir',
1400
				'writable_on' => 'restrictive',
1401
			);
1402
		}
1403
1404
		// Load up any custom themes.
1405
		require_once(SUBSDIR . '/Themes.subs.php');
1406
		$themes = getCustomThemes();
1407
		foreach ($themes as $id => $theme)
1408
		{
1409
			// Skip the default
1410
			if ($id == 1)
1411
				continue;
1412
1413
			if (substr(strtolower(strtr($theme['theme_dir'], array('\\' => '/'))), 0, strlen(BOARDDIR) + 7) === strtolower(strtr(BOARDDIR, array('\\' => '/')) . '/themes'))
1414
			{
1415
				$context['file_tree'][strtr(BOARDDIR, array('\\' => '/'))]['contents']['themes']['contents'][substr($theme['theme_dir'], strlen(BOARDDIR) + 8)] = array(
1416
					'type' => 'dir_recursive',
1417
					'list_contents' => true,
1418
					'contents' => array(
1419
						'languages' => array(
1420
							'type' => 'dir',
1421
							'list_contents' => true,
1422
						),
1423
					),
1424
				);
1425
			}
1426
			else
1427
			{
1428
				$context['file_tree'][strtr($theme['theme_dir'], array('\\' => '/'))] = array(
1429
					'type' => 'dir_recursive',
1430
					'list_contents' => true,
1431
					'contents' => array(
1432
						'languages' => array(
1433
							'type' => 'dir',
1434
							'list_contents' => true,
1435
						),
1436
					),
1437
				);
1438
			}
1439
		}
1440
1441
		// If we're submitting then let's move on to another function to keep things cleaner..
1442
		if (isset($this->_req->post->action_changes))
1443
			return $this->action_perms_save();
1444
1445
		$context['look_for'] = array();
1446
1447
		// Are we looking for a particular tree - normally an expansion?
1448
		if (!empty($this->_req->query->find))
1449
			$context['look_for'][] = base64_decode($this->_req->query->find);
1450
1451
		// Only that tree?
1452
		$context['only_find'] = isset($this->_req->query->xml) && !empty($this->_req->query->onlyfind) ? $this->_req->query->onlyfind : '';
1453
		if ($context['only_find'])
1454
			$context['look_for'][] = $context['only_find'];
1455
1456
		// Have we got a load of back-catalogue trees to expand from a submit etc?
1457
		if (!empty($this->_req->query->back_look))
1458
		{
1459
			$potententialTrees = json_decode(base64_decode($this->_req->query->back_look), true);
1460
			foreach ($potententialTrees as $tree)
1461
				$context['look_for'][] = $tree;
1462
		}
1463
1464
		// ... maybe posted?
1465
		if (!empty($this->_req->post->back_look))
1466
			$context['look_for'] = array_merge($context['look_for'], $this->_req->post->back_look);
1467
1468
		$context['back_look_data'] = base64_encode(json_encode(array_slice($context['look_for'], 0, 15)));
1469
1470
		// Are we finding more files than first thought?
1471
		$context['file_offset'] = !empty($this->_req->query->fileoffset) ? (int) $this->_req->query->fileoffset : 0;
1472
1473
		// Don't list more than this many files in a directory.
1474
		$context['file_limit'] = 150;
1475
1476
		// How many levels shall we show?
1477
		$context['default_level'] = empty($context['only_find']) ? 2 : 25;
1478
1479
		// This will be used if we end up catching XML data.
1480
		$context['xml_data'] = array(
1481
			'roots' => array(
1482
				'identifier' => 'root',
1483
				'children' => array(
1484
					array(
1485
						'value' => preg_replace('~[^A-Za-z0-9_\-=:]~', ':-:', $context['only_find']),
1486
					),
1487
				),
1488
			),
1489
			'folders' => array(
1490
				'identifier' => 'folder',
1491
				'children' => array(),
1492
			),
1493
		);
1494
1495
		foreach ($context['file_tree'] as $path => $data)
1496
		{
1497
			// Run this directory.
1498
			if (file_exists($path) && (empty($context['only_find']) || substr($context['only_find'], 0, strlen($path)) == $path))
1499
			{
1500
				// Get the first level down only.
1501
				fetchPerms__recursive($path, $context['file_tree'][$path], 1);
1502
				$context['file_tree'][$path]['perms'] = array(
1503
					'chmod' => @is_writable($path),
1504
					'perms' => @fileperms($path),
1505
				);
1506
			}
1507
			else
1508
				unset($context['file_tree'][$path]);
1509
		}
1510
1511
		// Is this actually xml?
1512
		if (isset($this->_req->query->xml))
1513
		{
1514
			theme()->getTemplates()->load('Xml');
1515
			$context['sub_template'] = 'generic_xml';
1516
			theme()->getLayers()->removeAll();
1517
		}
1518
	}
1519
1520
	/**
1521
	 * Actually action the permission changes they want.
1522
	 */
1523
	public function action_perms_save()
1524
	{
1525
		global $context, $txt, $time_start, $package_ftp;
1526
1527
		umask(0);
1528
1529
		$timeout_limit = 5;
1530
		$context['method'] = $this->_req->post->method === 'individual' ? 'individual' : 'predefined';
1531
		$context['back_look_data'] = isset($this->_req->post->back_look) ? $this->_req->post->back_look : array();
1532
1533
		// Skipping use of FTP?
1534
		if (empty($package_ftp))
1535
			$context['skip_ftp'] = true;
1536
1537
		// We'll start off in a good place, security. Make sure that if we're dealing with individual files that they seem in the right place.
1538
		if ($context['method'] === 'individual')
1539
		{
1540
			// Only these path roots are legal.
1541
			$legal_roots = array_keys($context['file_tree']);
1542
			$context['custom_value'] = (int) $this->_req->post->custom_value;
1543
1544
			// Continuing?
1545
			if (isset($this->_req->post->toProcess))
1546
				$this->_req->post->permStatus = json_decode(base64_decode($this->_req->post->toProcess), true);
1547
1548
			if (isset($this->_req->post->permStatus))
1549
			{
1550
				$context['to_process'] = array();
1551
				$validate_custom = false;
1552
				foreach ($this->_req->post->permStatus as $path => $status)
1553
				{
1554
					// Nothing to see here?
1555
					if ($status === 'no_change')
1556
						continue;
1557
1558
					$legal = false;
1559
					foreach ($legal_roots as $root)
1560
						if (substr($path, 0, strlen($root)) == $root)
1561
							$legal = true;
1562
1563
					if (!$legal)
1564
						continue;
1565
1566
					// Check it exists.
1567
					if (!file_exists($path))
1568
						continue;
1569
1570
					if ($status === 'custom')
1571
						$validate_custom = true;
1572
1573
					// Now add it.
1574
					$context['to_process'][$path] = $status;
1575
				}
1576
				$context['total_items'] = isset($this->_req->post->totalItems) ? (int) $this->_req->post->totalItems : count($context['to_process']);
1577
1578
				// Make sure the chmod status is valid?
1579
				if ($validate_custom)
1580
				{
1581
					if (!preg_match('~^[4567][4567][4567]$~', $context['custom_value']))
1582
						throw new \ElkArte\Exceptions\Exception($txt['chmod_value_invalid']);
1583
				}
1584
1585
				// Nothing to do?
1586
				if (empty($context['to_process']))
1587
					redirectexit('action=admin;area=packages;sa=perms' . (!empty($context['back_look_data']) ? ';back_look=' . base64_encode(json_encode($context['back_look_data'])) : '') . ';' . $context['session_var'] . '=' . $context['session_id']);
1588
			}
1589
			// Should never get here,
1590
			else
1591
				throw new \ElkArte\Exceptions\Exception('no_access', false);
1592
1593
			// Setup the custom value.
1594
			$custom_value = octdec('0' . $context['custom_value']);
1595
1596
			// Start processing items.
1597
			foreach ($context['to_process'] as $path => $status)
1598
			{
1599
				if (in_array($status, array('execute', 'writable', 'read')))
1600
					package_chmod($path, $status);
1601
				elseif ($status === 'custom' && !empty($custom_value))
1602
				{
1603
					// Use FTP if we have it.
1604
					if (!empty($package_ftp) && !empty($_SESSION['pack_ftp']))
1605
					{
1606
						$ftp_file = strtr($path, array($_SESSION['pack_ftp']['root'] => ''));
1607
						$package_ftp->chmod($ftp_file, $custom_value);
1608
					}
1609
					else
1610
						@chmod($path, $custom_value);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1611
				}
1612
1613
				// This fish is fried...
1614
				unset($context['to_process'][$path]);
1615
1616
				// See if we're out of time?
1617
				if (time() - array_sum(explode(' ', $time_start)) > $timeout_limit)
1618
					pausePermsSave();
1619
			}
1620
		}
1621
		// If predefined this is a little different.
1622
		else
1623
		{
1624
			$context['predefined_type'] = $this->_req->getPost('predefined', 'trim|strval', 'restricted');
1625
			$context['total_items'] = $this->_req->getPost('totalItems', 'intval', 0);
1626
			$context['directory_list'] = isset($this->_req->post->dirList) ? json_decode(base64_decode($this->_req->post->dirList), true) : array();
1627
			$context['file_offset'] = $this->_req->getPost('fileOffset', 'intval', 0);
1628
1629
			// Haven't counted the items yet?
1630
			if (empty($context['total_items']))
1631
			{
1632
				foreach ($context['file_tree'] as $path => $data)
1633
				{
1634
					if (is_dir($path))
1635
					{
1636
						$context['directory_list'][$path] = 1;
1637
						$context['total_items'] += $this->count_directories__recursive($path);
1638
						$context['total_items']++;
1639
					}
1640
				}
1641
			}
1642
1643
			// Have we built up our list of special files?
1644
			if (!isset($this->_req->post->specialFiles) && $context['predefined_type'] !== 'free')
1645
			{
1646
				$context['special_files'] = array();
1647
1648
				foreach ($context['file_tree'] as $path => $data)
1649
					$this->build_special_files__recursive($path, $data);
1650
			}
1651
			// Free doesn't need special files.
1652
			elseif ($context['predefined_type'] === 'free')
1653
				$context['special_files'] = array();
1654
			else
1655
				$context['special_files'] = json_decode(base64_decode($this->_req->post->specialFiles), true);
1656
1657
			// Now we definitely know where we are, we need to go through again doing the chmod!
1658
			foreach ($context['directory_list'] as $path => $dummy)
1659
			{
1660
				// Do the contents of the directory first.
1661
				try
1662
				{
1663
					$file_count = 0;
1664
					$dont_chmod = false;
1665
1666
					$entrys = new \FilesystemIterator($path, \FilesystemIterator::SKIP_DOTS);
1667
					foreach ($entrys as $entry)
1668
					{
1669
						$file_count++;
1670
1671
						// Actually process this file?
1672
						if (!$dont_chmod && !$entry->isDir() && (empty($context['file_offset']) || $context['file_offset'] < $file_count))
1673
						{
1674
							$status = $context['predefined_type'] === 'free' || isset($context['special_files'][$entry->getPathname()]) ? 'writable' : 'execute';
1675
							package_chmod($entry->getPathname(), $status);
1676
						}
1677
1678
						// See if we're out of time?
1679
						if (!$dont_chmod && time() - array_sum(explode(' ', $time_start)) > $timeout_limit)
1680
						{
1681
							$dont_chmod = true;
1682
1683
							// Make note of how far we have come so we restart at the right point
1684
							$context['file_offset'] = isset($file_count) ? $file_count : 0;
1685
							break;
1686
						}
1687
					}
1688
				}
1689
				catch (\UnexpectedValueException $e)
1690
				{
1691
					// @todo for now do nothing...
1692
				}
1693
1694
				// If this is set it means we timed out half way through.
1695
				if (!empty($dont_chmod))
1696
				{
1697
					$context['total_files'] = isset($file_count) ? $file_count : 0;
1698
					pausePermsSave();
1699
				}
1700
1701
				// Do the actual directory.
1702
				$status = $context['predefined_type'] === 'free' || isset($context['special_files'][$path]) ? 'writable' : 'execute';
1703
				package_chmod($path, $status);
1704
1705
				// We've finished the directory so no file offset, and no record.
1706
				$context['file_offset'] = 0;
1707
				unset($context['directory_list'][$path]);
1708
1709
				// See if we're out of time?
1710
				if (time() - array_sum(explode(' ', $time_start)) > $timeout_limit)
1711
					pausePermsSave();
1712
			}
1713
		}
1714
1715
		// If we're here we are done!
1716
		redirectexit('action=admin;area=packages;sa=perms' . (!empty($context['back_look_data']) ? ';back_look=' . base64_encode(json_encode($context['back_look_data'])) : '') . ';' . $context['session_var'] . '=' . $context['session_id']);
1717
	}
1718
1719
	/**
1720
	 * Builds a list of special files recursively for a given path
1721
	 *
1722
	 * @param string $path
1723
	 * @param mixed[] $data
1724
	 */
1725
	public function build_special_files__recursive($path, &$data)
1726
	{
1727
		global $context;
1728
1729
		if (!empty($data['writable_on']))
1730
			if ($context['predefined_type'] === 'standard' || $data['writable_on'] === 'restrictive')
1731
				$context['special_files'][$path] = 1;
1732
1733
		if (!empty($data['contents']))
1734
			foreach ($data['contents'] as $name => $contents)
1735
				$this->build_special_files__recursive($path . '/' . $name, $contents);
1736
	}
1737
1738
	/**
1739
	 * Recursive counts all the directory's under a given path
1740
	 *
1741
	 * @param string $dir
1742
	 *
1743
	 * @return int
1744
	 */
1745
	public function count_directories__recursive($dir)
1746
	{
1747
		global $context;
1748
1749
		$count = 0;
1750
1751
		try
1752
		{
1753
			$iterator = new \RecursiveIteratorIterator(
1754
				new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
1755
				\RecursiveIteratorIterator::SELF_FIRST,
1756
				\RecursiveIteratorIterator::CATCH_GET_CHILD
1757
			);
1758
1759
			foreach ($iterator as $path => $file)
1760
			{
1761
				if ($file->isDir())
1762
				{
1763
					$context['directory_list'][$path] = 1;
1764
					$count++;
1765
				}
1766
			}
1767
		}
1768
		catch (\UnexpectedValueException $e)
1769
		{
1770
			// @todo
1771
		}
1772
1773
		return $count;
1774
	}
1775
1776
	/**
1777
	 * Get a listing of all the packages
1778
	 *
1779
	 * - Determines if the package is a ddon, smiley, avatar, language or unknown package
1780
	 * - Determines if the package has been installed or not
1781
	 *
1782
	 * @param int $start The item to start with (for pagination purposes)
1783
	 * @param int $items_per_page The number of items to show per page
1784
	 * @param string $sort A string indicating how to sort the results
1785
	 * @param string $params 'type' type of package
1786
	 * @param bool $installed
1787
	 *
1788
	 * @return mixed
1789
	 * @throws \ElkArte\Exceptions\Exception
1790
	 */
1791
	public function list_packages($start, $items_per_page, $sort, $params, $installed)
1792
	{
1793
		global $scripturl, $context;
1794
		static $instadds, $packages;
1795
1796
		// Start things up
1797
		if (!isset($packages[$params]))
1798
			$packages[$params] = array();
1799
1800
		// We need the packages directory to be writable for this.
1801
		if (!@is_writable(BOARDDIR . '/packages'))
1802
			create_chmod_control(array(BOARDDIR . '/packages'), array('destination_url' => $scripturl . '?action=admin;area=packages', 'crash_on_error' => true));
1803
1804
		list ($the_brand, $the_version) = explode(' ', FORUM_VERSION, 2);
1805
1806
		// Here we have a little code to help those who class themselves as something of gods, version emulation ;)
1807
		if (isset($this->_req->query->version_emulate) && strtr($this->_req->query->version_emulate, array($the_brand => '')) == $the_version)
1808
			unset($_SESSION['version_emulate']);
1809
		elseif (isset($this->_req->query->version_emulate))
1810
		{
1811
			if (($this->_req->query->version_emulate === 0 || $this->_req->query->version_emulate === FORUM_VERSION) && isset($this->_req->session->version_emulate))
1812
				unset($_SESSION['version_emulate']);
1813
			elseif ($this->_req->query->version_emulate !== 0)
1814
				$_SESSION['version_emulate'] = strtr($this->_req->query->version_emulate, array('-' => ' ', '+' => ' ', $the_brand . ' ' => ''));
1815
		}
1816
1817
		if (!empty($_SESSION['version_emulate']))
1818
		{
1819
			$context['forum_version'] = $the_brand . ' ' . $_SESSION['version_emulate'];
1820
			$the_version = $_SESSION['version_emulate'];
1821
		}
1822
1823
		if (isset($_SESSION['single_version_emulate']))
1824
			unset($_SESSION['single_version_emulate']);
1825
1826
		if (empty($instadds))
1827
		{
1828
			$instadds = loadInstalledPackages();
1829
			$installed_adds = array();
1830
1831
			// Look through the list of installed mods...
1832
			foreach ($instadds as $installed_add)
1833
				$installed_adds[$installed_add['package_id']] = array(
1834
					'id' => $installed_add['id'],
1835
					'version' => $installed_add['version'],
1836
				);
1837
1838
			// Get a list of all the ids installed, so the latest packages won't include already installed ones.
1839
			$context['installed_adds'] = array_keys($installed_adds);
1840
		}
1841
1842
		if ($installed)
1843
		{
1844
			$sort_id = 1;
1845
			foreach ($instadds as $installed_add)
1846
			{
1847
				$context['available_addon'][$installed_add['package_id']] = array(
1848
					'sort_id' => $sort_id++,
1849
					'can_uninstall' => true,
1850
					'name' => $installed_add['name'],
1851
					'filename' => $installed_add['filename'],
1852
					'installed_id' => $installed_add['id'],
1853
					'version' => $installed_add['version'],
1854
					'is_installed' => true,
1855
					'is_current' => true,
1856
				);
1857
			}
1858
		}
1859
1860
		if (empty($packages))
1861
			foreach ($context['package_types'] as $type)
0 ignored issues
show
Bug introduced by
The expression $context['package_types'] of type string|array<integer,integer|string> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1862
				$packages[$type] = array();
1863
1864
		try
1865
		{
1866
			$dir = new \FilesystemIterator(BOARDDIR . '/packages', \FilesystemIterator::SKIP_DOTS);
1867
			$filtered_dir = new \ElkArte\PackagesFilterIterator($dir);
1868
1869
			$dirs = array();
1870
			$sort_id = array(
1871
				'addon' => 1,
1872
				'avatar' => 1,
1873
				'language' => 1,
1874
				'smiley' => 1,
1875
				'unknown' => 1,
1876
			);
1877
			foreach ($filtered_dir as $package)
1878
			{
1879
				foreach ($context['package_types'] as $type)
0 ignored issues
show
Bug introduced by
The expression $context['package_types'] of type string|array<integer,integer|string> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1880
					if (isset($context['available_' . $type][md5($package->getFilename())]))
1881
						continue 2;
1882
1883
				// Skip directories or files that are named the same.
1884
				if ($package->isDir())
1885
				{
1886
					if (in_array($package, $dirs))
1887
						continue;
1888
					$dirs[] = $package;
1889
				}
1890
				elseif (substr(strtolower($package->getFilename()), -7) === '.tar.gz')
1891
				{
1892
					if (in_array(substr($package, 0, -7), $dirs))
1893
						continue;
1894
					$dirs[] = substr($package, 0, -7);
1895
				}
1896
				elseif (strtolower($package->getExtension()) === 'zip' || strtolower($package->getExtension()) === 'tgz')
1897
				{
1898
					if (in_array(substr($package->getBasename(), 0, -4), $dirs))
1899
						continue;
1900
					$dirs[] = substr($package->getBasename(), 0, -4);
1901
				}
1902
1903
				$packageInfo = getPackageInfo($package->getFilename());
1904
				if (!is_array($packageInfo))
1905
					continue;
1906
1907
				if (!empty($packageInfo))
1908
				{
1909
					$packageInfo['installed_id'] = isset($installed_adds[$packageInfo['id']]) ? $installed_adds[$packageInfo['id']]['id'] : 0;
1910
					$packageInfo['sort_id'] = isset($sort_id[$packageInfo['type']]) ? $sort_id[$packageInfo['type']] : $sort_id['unknown'];
1911
					$packageInfo['is_installed'] = isset($installed_adds[$packageInfo['id']]);
1912
					$packageInfo['is_current'] = $packageInfo['is_installed'] && isset($installed_adds[$packageInfo['id']]) && ($installed_adds[$packageInfo['id']]['version'] == $packageInfo['version']);
1913
					$packageInfo['is_newer'] = $packageInfo['is_installed'] && isset($installed_adds[$packageInfo['id']]) && ($installed_adds[$packageInfo['id']]['version'] > $packageInfo['version']);
1914
					$packageInfo['can_install'] = false;
1915
					$packageInfo['can_uninstall'] = false;
1916
					$packageInfo['can_upgrade'] = false;
1917
					$packageInfo['can_emulate_install'] = false;
1918
					$packageInfo['can_emulate_uninstall'] = false;
1919
1920
					// This package is currently NOT installed.  Check if it can be.
1921
					if (!$packageInfo['is_installed'] && $packageInfo['xml']->exists('install'))
1922
					{
1923
						// Check if there's an install for *THIS* version
1924
						$installs = $packageInfo['xml']->set('install');
1925 View Code Duplication
						foreach ($installs as $install)
1926
						{
1927
							if (!$install->exists('@for') || matchPackageVersion($the_version, $install->fetch('@for')))
1928
							{
1929
								// Okay, this one is good to go.
1930
								$packageInfo['can_install'] = true;
1931
								break;
1932
							}
1933
						}
1934
1935
						// no install found for our version, lets see if one exists for another
1936 View Code Duplication
						if ($packageInfo['can_install'] === false && $install->exists('@for') && empty($_SESSION['version_emulate']))
0 ignored issues
show
Bug introduced by
The variable $install does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1937
						{
1938
							$reset = true;
1939
1940
							// Get the highest install version that is available from the package
1941
							foreach ($installs as $install)
1942
							{
1943
								$packageInfo['can_emulate_install'] = matchHighestPackageVersion($install->fetch('@for'), $reset, $the_version);
1944
								$reset = false;
1945
							}
1946
						}
1947
					}
1948
					// An already installed, but old, package.  Can we upgrade it?
1949
					elseif ($packageInfo['is_installed'] && !$packageInfo['is_current'] && $packageInfo['xml']->exists('upgrade'))
1950
					{
1951
						$upgrades = $packageInfo['xml']->set('upgrade');
1952
1953
						// First go through, and check against the current version of ElkArte.
1954
						foreach ($upgrades as $upgrade)
1955
						{
1956
							// Even if it is for this ElkArte, is it for the installed version of the mod?
1957
							if (!$upgrade->exists('@for') || matchPackageVersion($the_version, $upgrade->fetch('@for')))
1958
								if (!$upgrade->exists('@from') || matchPackageVersion($installed_adds[$packageInfo['id']]['version'], $upgrade->fetch('@from')))
0 ignored issues
show
Bug introduced by
The variable $installed_adds does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1959
								{
1960
									$packageInfo['can_upgrade'] = true;
1961
									break;
1962
								}
1963
						}
1964
					}
1965
					// Note that it has to be the current version to be uninstallable.  Shucks.
1966
					elseif ($packageInfo['is_installed'] && $packageInfo['is_current'] && $packageInfo['xml']->exists('uninstall'))
1967
					{
1968
						$uninstalls = $packageInfo['xml']->set('uninstall');
1969
1970
						// Can we find any uninstallation methods that work for this ElkArte version?
1971 View Code Duplication
						foreach ($uninstalls as $uninstall)
1972
						{
1973
							if (!$uninstall->exists('@for') || matchPackageVersion($the_version, $uninstall->fetch('@for')))
1974
							{
1975
								$packageInfo['can_uninstall'] = true;
1976
								break;
1977
							}
1978
						}
1979
1980
						// No uninstall found for this version, lets see if one exists for another
1981 View Code Duplication
						if ($packageInfo['can_uninstall'] === false && $uninstall->exists('@for') && empty($_SESSION['version_emulate']))
0 ignored issues
show
Bug introduced by
The variable $uninstall does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1982
						{
1983
							$reset = true;
1984
1985
							// Get the highest install version that is available from the package
1986
							foreach ($uninstalls as $uninstall)
1987
							{
1988
								$packageInfo['can_emulate_uninstall'] = matchHighestPackageVersion($uninstall->fetch('@for'), $reset, $the_version);
1989
								$reset = false;
1990
							}
1991
						}
1992
					}
1993
1994
					// Add-on / Modification
1995
					if ($packageInfo['type'] === 'addon' || $packageInfo['type'] === 'modification' || $packageInfo['type'] === 'mod')
1996
					{
1997
						$sort_id['addon']++;
1998
						if ($installed)
1999
						{
2000
							if (!empty($context['available_addon'][$packageInfo['id']]))
2001
							{
2002
								$packages['modification'][strtolower($packageInfo[$sort]) . '_' . $sort_id['mod']] = $packageInfo['id'];
2003
								$context['available_addon'][$packageInfo['id']] = array_merge($context['available_addon'][$packageInfo['id']], $packageInfo);
2004
							}
2005
						}
2006 View Code Duplication
						else
2007
						{
2008
							$packages['addon'][strtolower($packageInfo[$sort]) . '_' . $sort_id['addon']] = md5($package->getFilename());
2009
							$context['available_addon'][md5($package->getFilename())] = $packageInfo;
2010
						}
2011
					}
2012
					// Avatar package.
2013 View Code Duplication
					elseif ($packageInfo['type'] === 'avatar')
2014
					{
2015
						$sort_id[$packageInfo['type']]++;
2016
						$packages['avatar'][strtolower($packageInfo[$sort]) . '_' . $sort_id['avatar']] = md5($package->getFilename());
2017
						$context['available_avatar'][md5($package->getFilename())] = $packageInfo;
2018
					}
2019
					// Smiley package.
2020 View Code Duplication
					elseif ($packageInfo['type'] === 'smiley')
2021
					{
2022
						$sort_id[$packageInfo['type']]++;
2023
						$packages['smiley'][strtolower($packageInfo[$sort]) . '_' . $sort_id['smiley']] = md5($package->getFilename());
2024
						$context['available_smiley'][md5($package->getFilename())] = $packageInfo;
2025
					}
2026
					// Language package.
2027 View Code Duplication
					elseif ($packageInfo['type'] === 'language')
2028
					{
2029
						$sort_id[$packageInfo['type']]++;
2030
						$packages['language'][strtolower($packageInfo[$sort]) . '_' . $sort_id['language']] = md5($package->getFilename());
2031
						$context['available_language'][md5($package->getFilename())] = $packageInfo;
2032
					}
2033
					// Other stuff.
2034 View Code Duplication
					else
2035
					{
2036
						$sort_id['unknown']++;
2037
						$packages['unknown'][strtolower($packageInfo[$sort]) . '_' . $sort_id['unknown']] = md5($package->getFilename());
2038
						$context['available_unknown'][md5($package->getFilename())] = $packageInfo;
2039
					}
2040
				}
2041
			}
2042
		}
2043
		catch (\UnexpectedValueException $e)
2044
		{
2045
			// @todo for now do nothing...
2046
		}
2047
2048
		if (isset($this->_req->query->desc))
2049
			krsort($packages[$params]);
2050
		else
2051
			ksort($packages[$params]);
2052
2053
		return $packages[$params];
2054
	}
2055
}
2056
2057
/**
2058
 * Checks the permissions of all the areas that will be affected by the package
2059
 *
2060
 * @package Packages
2061
 *
2062
 * @param string  $path
2063
 * @param mixed[] $data
2064
 * @param int     $level
2065
 *
2066
 * @throws \ElkArte\Exceptions\Exception no_access
2067
 */
2068
function fetchPerms__recursive($path, &$data, $level)
2069
{
2070
	global $context;
2071
2072
	$isLikelyPath = false;
2073
	foreach ($context['look_for'] as $possiblePath)
2074
	{
2075
		if (substr($possiblePath, 0, strlen($path)) == $path)
2076
			$isLikelyPath = true;
2077
	}
2078
2079
	// Is this where we stop?
2080
	if (isset($_GET['xml']) && !empty($context['look_for']) && !$isLikelyPath)
2081
		return;
2082
	elseif ($level > $context['default_level'] && !$isLikelyPath)
2083
		return;
2084
2085
	// Are we actually interested in saving this data?
2086
	$save_data = empty($context['only_find']) || $context['only_find'] == $path;
2087
2088
	// @todo Shouldn't happen - but better error message?
2089
	if (!is_dir($path))
2090
		throw new \ElkArte\Exceptions\Exception('no_access', false);
2091
2092
	// This is where we put stuff we've found for sorting.
2093
	$foundData = array(
2094
		'files' => array(),
2095
		'folders' => array(),
2096
	);
2097
2098
	try
2099
	{
2100
		$entrys = new \FilesystemIterator($path, \FilesystemIterator::SKIP_DOTS);
2101
		foreach ($entrys as $entry)
2102
		{
2103
			// Some kind of file?
2104
			if ($entry->isFile())
2105
			{
2106
				// Are we listing PHP files in this directory?
2107
				if ($save_data && !empty($data['list_contents']) && $entry->getExtension() === 'php')
2108
					$foundData['files'][$entry->getFilename()] = true;
2109
				// A file we were looking for.
2110
				elseif ($save_data && isset($data['contents'][$entry->getFilename()]))
2111
					$foundData['files'][$entry->getFilename()] = true;
2112
			}
2113
			// It's a directory - we're interested one way or another, probably...
2114
			elseif ($entry->isDir())
2115
			{
2116
				// Going further?
2117
				if ((!empty($data['type']) && $data['type'] === 'dir_recursive')
2118
					|| (isset($data['contents'][$entry->getFilename()])
2119
						&& (!empty($data['contents'][$entry->getFilename()]['list_contents'])
2120
							|| (!empty($data['contents'][$entry->getFilename()]['type'])
2121
								&& $data['contents'][$entry->getFilename()]['type'] === 'dir_recursive'))))
2122
				{
2123
					if (!isset($data['contents'][$entry->getFilename()]))
2124
						$foundData['folders'][$entry->getFilename()] = 'dir_recursive';
2125
					else
2126
						$foundData['folders'][$entry->getFilename()] = true;
2127
2128
					// If this wasn't expected inherit the recusiveness...
2129
					if (!isset($data['contents'][$entry->getFilename()]))
2130
					{
2131
						// We need to do this as we will be going all recursive.
2132
						$data['contents'][$entry->getFilename()] = array(
2133
							'type' => 'dir_recursive',
2134
						);
2135
					}
2136
2137
					// Actually do the recursive stuff...
2138
					fetchPerms__recursive($entry->getPathname(), $data['contents'][$entry->getFilename()], $level + 1);
2139
				}
2140
				// Maybe it is a folder we are not descending into.
2141
				elseif (isset($data['contents'][$entry->getFilename()]))
2142
					$foundData['folders'][$entry->getFilename()] = true;
2143
				// Otherwise we stop here.
2144
			}
2145
		}
2146
	}
2147
	catch (\UnexpectedValueException $e)
2148
	{
2149
		// @todo for now do nothing...
2150
	}
2151
2152
	// Sort the output so it presents itself well in the template
2153
	uksort($foundData['folders'], 'strcasecmp');
2154
	uksort($foundData['files'], 'strcasecmp');
2155
2156
	// Nothing to see here?
2157
	if (!$save_data)
2158
		return;
2159
2160
	// Now actually add the data, starting with the folders.
2161
	foreach ($foundData['folders'] as $folder => $type)
2162
	{
2163
		$additional_data = array(
2164
			'perms' => array(
2165
				'chmod' => @is_writable($path . '/' . $folder),
2166
				'perms' => @fileperms($path . '/' . $folder),
2167
			),
2168
		);
2169
		if ($type !== true)
2170
			$additional_data['type'] = $type;
2171
2172
		// If there's an offset ignore any folders in XML mode.
2173
		if (isset($_GET['xml']) && $context['file_offset'] == 0)
2174
		{
2175
			$context['xml_data']['folders']['children'][] = array(
2176
				'attributes' => array(
2177
					'writable' => $additional_data['perms']['chmod'] ? 1 : 0,
2178
					'permissions' => substr(sprintf('%o', $additional_data['perms']['perms']), -4),
2179
					'folder' => 1,
2180
					'path' => $context['only_find'],
2181
					'level' => $level,
2182
					'more' => 0,
2183
					'offset' => $context['file_offset'],
2184
					'my_ident' => preg_replace('~[^A-Za-z0-9_\-=:]~', ':-:', $context['only_find'] . '/' . $folder),
2185
					'ident' => preg_replace('~[^A-Za-z0-9_\-=:]~', ':-:', $context['only_find']),
2186
				),
2187
				'value' => $folder,
2188
			);
2189
		}
2190 View Code Duplication
		elseif (!isset($_GET['xml']))
2191
		{
2192
			if (isset($data['contents'][$folder]))
2193
				$data['contents'][$folder] = array_merge($data['contents'][$folder], $additional_data);
2194
			else
2195
				$data['contents'][$folder] = $additional_data;
2196
		}
2197
	}
2198
2199
	// Now we want to do a similar thing with files.
2200
	$counter = -1;
2201
	foreach ($foundData['files'] as $file => $dummy)
2202
	{
2203
		$counter++;
2204
2205
		// Have we reached our offset?
2206
		if ($context['file_offset'] > $counter)
2207
			continue;
2208
2209
		// Gone too far?
2210
		if ($counter > ($context['file_offset'] + $context['file_limit']))
2211
			break;
2212
2213
		$additional_data = array(
2214
			'perms' => array(
2215
				'chmod' => @is_writable($path . '/' . $file),
2216
				'perms' => @fileperms($path . '/' . $file),
2217
			),
2218
		);
2219
2220
		// XML?
2221
		if (isset($_GET['xml']))
2222
		{
2223
			$context['xml_data']['folders']['children'][] = array(
2224
				'attributes' => array(
2225
					'writable' => $additional_data['perms']['chmod'] ? 1 : 0,
2226
					'permissions' => substr(sprintf('%o', $additional_data['perms']['perms']), -4),
2227
					'folder' => 0,
2228
					'path' => $context['only_find'],
2229
					'level' => $level,
2230
					'more' => $counter == ($context['file_offset'] + $context['file_limit']) ? 1 : 0,
2231
					'offset' => $context['file_offset'],
2232
					'my_ident' => preg_replace('~[^A-Za-z0-9_\-=:]~', ':-:', $context['only_find'] . '/' . $file),
2233
					'ident' => preg_replace('~[^A-Za-z0-9_\-=:]~', ':-:', $context['only_find']),
2234
				),
2235
				'value' => $file,
2236
			);
2237
		}
2238
		elseif ($counter != ($context['file_offset'] + $context['file_limit']))
2239
		{
2240 View Code Duplication
			if (isset($data['contents'][$file]))
2241
				$data['contents'][$file] = array_merge($data['contents'][$file], $additional_data);
2242
			else
2243
				$data['contents'][$file] = $additional_data;
2244
		}
2245
	}
2246
}
2247
2248
/**
2249
 * Function called to briefly pause execution of directory/file chmod actions
2250
 *
2251
 * - Called by action_perms_save().
2252
 *
2253
 * @package Packages
2254
 */
2255
function pausePermsSave()
2256
{
2257
	global $context, $txt;
2258
2259
	// Try get more time...
2260
	detectServer()->setTimeLimit(600);
2261
2262
	// Set up the items for the pause form
2263
	$context['sub_template'] = 'pause_action_permissions';
2264
	$context['page_title'] = $txt['package_file_perms_applying'];
2265
2266
	// And how are we progressing with our directories
2267
	$context['remaining_items'] = count($context['method'] === 'individual' ? $context['to_process'] : $context['directory_list']);
2268
	$context['progress_message'] = sprintf($context['method'] === 'individual' ? $txt['package_file_perms_items_done'] : $txt['package_file_perms_dirs_done'], $context['total_items'] - $context['remaining_items'], $context['total_items']);
2269
	$context['progress_percent'] = round(($context['total_items'] - $context['remaining_items']) / $context['total_items'] * 100, 1);
2270
2271
	// Never more than 100%!
2272
	$context['progress_percent'] = min($context['progress_percent'], 100);
2273
2274
	// And how are we progressing with files within a directory
2275
	if ($context['method'] !== 'individual' && !empty($context['total_files']))
2276
	{
2277
		$context['file_progress_message'] = sprintf($txt['package_file_perms_files_done'], $context['file_offset'], $context['total_files']);
2278
		$context['file_progress_percent'] = round($context['file_offset'] / $context['total_files'] * 100, 1);
2279
2280
		// Never more than 100%!
2281
		$context['file_progress_percent'] = min($context['file_progress_percent'], 100);
2282
	}
2283
2284
	obExit();
2285
}
2286