parseModification()   F
last analyzed

Complexity

Conditions 77
Paths > 20000

Size

Total Lines 393
Code Lines 179

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 84
CRAP Score 950.2326

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 77
eloc 179
c 2
b 0
f 0
nc 3292933
nop 4
dl 0
loc 393
ccs 84
cts 178
cp 0.4719
crap 950.2326
rs 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This contains functions for handling tar.gz and .zip files
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
use ElkArte\Helper\FileFunctions;
18
use ElkArte\Helper\UnTgz;
19
use ElkArte\Helper\UnZip;
20
use ElkArte\Helper\Util;
21
use ElkArte\Http\CurlFetchWebdata;
22
use ElkArte\Http\FsockFetchWebdata;
23
use ElkArte\Http\FtpConnection;
24
use ElkArte\Http\StreamFetchWebdata;
25
use ElkArte\Packages\PackageChmod;
26
use ElkArte\Packages\PackageParser;
27
use ElkArte\User;
28
use ElkArte\XmlArray;
29
30
/**
31
 * Reads a .tar.gz file, filename, in and extracts file(s) from it.
32
 * essentially just a shortcut for read_tgz_data().
33
 *
34
 * @param string $gzfilename
35
 * @param string $destination
36
 * @param bool $single_file = false
37
 * @param bool $overwrite = false
38
 * @param string[]|null $files_to_extract = null
39
 * @return array|bool
40
 * @package Packages
41
 */
42 4
function read_tgz_file($gzfilename, $destination, $single_file = false, $overwrite = false, $files_to_extract = null)
43
{
44
	// From a web site
45
	if (str_starts_with($gzfilename, 'http://') || str_starts_with($gzfilename, 'https://'))
46
	{
47
		$data = fetch_web_data($gzfilename);
48
	}
49
	// Or a file on the system
50
	else
51
	{
52
		$data = @file_get_contents($gzfilename);
53
	}
54 4
55
	if ($data === false)
56 4
	{
57
		return false;
58
	}
59
60
	return read_tgz_data($data, $destination, $single_file, $overwrite, $files_to_extract);
61
}
62 4
63
/**
64
 * Extracts a file or files from the .tar.gz contained in data.
65
 *
66
 * - Detects if the file is really a .zip file, and if so returns the result of read_zip_data
67
 *
68
 * if destination is null
69
 * - returns a list of files in the archive.
70
 *
71
 * if single_file is true
72
 * - returns the contents of the file specified by destination, if it exists, or false.
73
 * - destination can start with * and / to signify that the file may come from any directory.
74
 * - destination should not begin with a / if single_file is true.
75
 *
76
 * - existing files with newer modification times if and only if overwrite is true.
77
 * - creates the destination directory if it doesn't exist, and is is specified.
78
 * - requires zlib support be built into PHP.
79
 * - returns an array of the files extracted on success
80
 * - if files_to_extract is not equal to null only extracts the files within this array.
81
 *
82
 * @param string $data
83
 * @param string $destination
84
 * @param bool $single_file = false,
85
 * @param bool $overwrite = false,
86
 * @param string[]|null $files_to_extract = null
87
 * @return array|bool
88
 * @package Packages
89
 */
90
function read_tgz_data($data, $destination, $single_file = false, $overwrite = false, $files_to_extract = null)
91
{
92
	$untgz = new UnTgz($data, $destination, $single_file, $overwrite, $files_to_extract);
93
94
	// Choose the right method for the file
95 4
	if ($untgz->check_valid_tgz())
96
	{
97
		return $untgz->read_tgz_data();
98 4
	}
99
100
	unset($untgz);
101
102
	return read_zip_data($data, $destination, $single_file, $overwrite, $files_to_extract);
103
}
104 4
105
/**
106 4
 * Extract zip data.
107
 *
108
 * - If destination is null, return a listing.
109
 *
110
 * @param string $data
111
 * @param string $destination
112
 * @param bool $single_file
113
 * @param bool $overwrite
114
 * @param string[]|null $files_to_extract
115
 * @return array|bool
116
 * @package Packages
117
 */
118
function read_zip_data($data, $destination, $single_file = false, $overwrite = false, $files_to_extract = null)
119
{
120
	$unzip = new UnZip($data, $destination, $single_file, $overwrite, $files_to_extract);
121
122
	return $unzip->read_zip_data();
123
}
124
125
/**
126 4
 * Loads and returns an array of installed packages.
127
 *
128 4
 * - Gets this information from packages/installed.list.
129
 * - Returns the array of data.
130
 * - Default sort order is package_installed time
131
 *
132
 * @return array
133
 * @package Packages
134
 */
135
function loadInstalledPackages()
136
{
137
	$db = database();
138
139
	// First, check that the database is valid, installed.list is still king.
140
	$install_file = @file_get_contents(BOARDDIR . '/packages/installed.list');
141
	if (trim($install_file) === '')
0 ignored issues
show
Bug introduced by
It seems like $install_file can also be of type false; however, parameter $string of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

141
	if (trim(/** @scrutinizer ignore-type */ $install_file) === '')
Loading history...
142
	{
143
		$db->query('', '
144
			UPDATE {db_prefix}log_packages
145
			SET 
146
				install_state = {int:not_installed}',
147
			[
148
				'not_installed' => 0,
149
			]
150
		);
151
152
		// Don't have anything left, so send an empty array.
153
		return [];
154
	}
155
156
	// Load the packages from the database - note this is ordered by installation time to ensure
157
	// latest package uninstalled first.
158
	$installed = [];
159
	$found = [];
160
	$db->fetchQuery('
161
		SELECT 
162
			id_install, package_id, filename, name, version, time_installed
163
		FROM {db_prefix}log_packages
164
		WHERE install_state != {int:not_installed}
165
		ORDER BY time_installed DESC',
166
		[
167
			'not_installed' => 0,
168
		]
169
	)->fetch_callback(
170
		function ($row) use (&$found, &$installed) {
171
			// Already found this? If so don't add it twice!
172
			if (in_array((int) $row['package_id'], $found, true))
173
			{
174
				return;
175
			}
176
177
			$found[] = $row['package_id'];
178
179
			$installed[] = [
180 2
				'id' => (int) $row['id_install'],
181
				'name' => $row['name'],
182
				'filename' => $row['filename'],
183 2
				'package_id' => $row['package_id'],
184 2
				'version' => $row['version'],
185
				'time_installed' => !empty($row['time_installed']) ? $row['time_installed'] : 0,
186 2
			];
187
		}
188
	);
189
190
	return $installed;
191 2
}
192
193
/**
194
 * Loads a package's information and returns a representative array.
195
 *
196 2
 * - Expects the file to be a package in packages/.
197
 * - Returns a error string if the package-info is invalid.
198
 * - Otherwise returns a basic array of id, version, filename, and similar information.
199
 * - An \ElkArte\XmlArray is available in 'xml'.
200
 *
201
 * @param string $gzFilename
202
 *
203
 * @return array|string error string on error array on success
204
 * @package Packages
205
 */
206
function getPackageInfo($gzFilename)
207
{
208
	$gzFilename = trim($gzFilename);
209
	$fileFunc = FileFunctions::instance();
210
211
	// Extract package-info.xml from downloaded file. (*/ is used because it could be in any directory.)
212
	if (preg_match('~^https?://~i', $gzFilename) === 1)
213
	{
214
		$packageInfo = read_tgz_data(fetch_web_data($gzFilename, '', true), '*/package-info.xml', true);
0 ignored issues
show
Bug introduced by
It seems like fetch_web_data($gzFilename, '', true) can also be of type false; however, parameter $data of read_tgz_data() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

214
		$packageInfo = read_tgz_data(/** @scrutinizer ignore-type */ fetch_web_data($gzFilename, '', true), '*/package-info.xml', true);
Loading history...
215
	}
216
	else
217
	{
218
		// It must be in the package directory then
219
		if (!$fileFunc->fileExists(BOARDDIR . '/packages/' . $gzFilename))
220
		{
221
			return 'package_get_error_not_found';
222
		}
223
224
		// Make sure a package.xml file is available
225
		if ($fileFunc->fileExists(BOARDDIR . '/packages/' . $gzFilename))
226
		{
227
			$packageInfo = read_tgz_file(BOARDDIR . '/packages/' . $gzFilename, '*/package-info.xml', true);
228
		}
229
		elseif ($fileFunc->fileExists(BOARDDIR . '/packages/' . $gzFilename . '/package-info.xml'))
230
		{
231
			$packageInfo = file_get_contents(BOARDDIR . '/packages/' . $gzFilename . '/package-info.xml');
232
		}
233
		else
234
		{
235
			return 'package_get_error_missing_xml';
236
		}
237
	}
238
239
	// Nothing?
240
	if (empty($packageInfo))
241
	{
242
		// Perhaps they are trying to install a theme, tell them, nicely, this is the wrong function
243
		$packageInfo = read_tgz_file(BOARDDIR . '/packages/' . $gzFilename, '*/theme_info.xml', true);
244
		if (!empty($packageInfo))
245
		{
246
			return 'package_get_error_is_theme';
247
		}
248
249
		return 'package_get_error_is_zero';
250 4
	}
251
252
	// Parse package-info.xml into an \ElkArte\XmlArray.
253 4
	$packageInfo = new XmlArray($packageInfo);
0 ignored issues
show
Bug introduced by
It seems like $packageInfo can also be of type array; however, parameter $data of ElkArte\XmlArray::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

253
	$packageInfo = new XmlArray(/** @scrutinizer ignore-type */ $packageInfo);
Loading history...
254
255 2
	if (!$packageInfo->exists('package-info[0]'))
256
	{
257
		return 'package_get_error_packageinfo_corrupt';
258
	}
259
260 4
	$packageInfo = $packageInfo->path('package-info[0]');
261
262
	// Convert packageInfo to an array for use
263
	$package = Util::htmlspecialchars__recursive($packageInfo->to_array());
264
	$package['xml'] = $packageInfo;
265
	$package['filename'] = $gzFilename;
266 4
267
	// Set a default type if none was supplied in the package
268 4
	if (!isset($package['type']))
269
	{
270
		$package['type'] = 'addon';
271
	}
272
273
	return $package;
274
}
275
276
/**
277
 * Create a chmod control for chmoding files.
278
 *
279
 * @param string[] $chmodFiles
280
 * @param array $chmodOptions
281 4
 * @param bool $restore_write_status
282
 * @return array|bool
283
 * @package Packages
284
 * @deprecated since 2.0, use PackageChmod class
285
 */
286
function create_chmod_control($chmodFiles = [], $chmodOptions = [], $restore_write_status = false)
287
{
288
	$create_chmod_control = new PackageChmod();
289
290
	return $create_chmod_control->createChmodControl($chmodFiles, $chmodOptions, $restore_write_status);
291
}
292
293
/**
294
 * Get a listing of files that will need to be set back to the original state
295
 *
296 4
 * @param string $dummy1
297
 * @param string $dummy2
298
 * @param string $dummy3
299 4
 * @param bool $do_change
300
 *
301
 * @return array
302
 */
303
function list_restoreFiles($dummy1, $dummy2, $dummy3, $do_change)
0 ignored issues
show
Unused Code introduced by
The parameter $dummy2 is not used and could be removed. ( Ignorable by Annotation )

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

303
function list_restoreFiles($dummy1, /** @scrutinizer ignore-unused */ $dummy2, $dummy3, $do_change)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $dummy3 is not used and could be removed. ( Ignorable by Annotation )

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

303
function list_restoreFiles($dummy1, $dummy2, /** @scrutinizer ignore-unused */ $dummy3, $do_change)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $dummy1 is not used and could be removed. ( Ignorable by Annotation )

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

303
function list_restoreFiles(/** @scrutinizer ignore-unused */ $dummy1, $dummy2, $dummy3, $do_change)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
304 4
{
305
	global $txt, $package_ftp;
306
307 4
	$restore_files = [];
308 4
	$fileFunc = FileFunctions::instance();
309 4
310
	foreach ($_SESSION['ftp_connection']['original_perms'] as $file => $perms)
311
	{
312 4
		// Check the file still exists, and the permissions were indeed different than now.
313
		$file_permissions = $fileFunc->filePerms($file);
314
		if (!$fileFunc->fileExists($file) || $file_permissions === $perms)
315
		{
316
			unset($_SESSION['ftp_connection']['original_perms'][$file]);
317 4
			continue;
318
		}
319
320
		// Are we wanting to change the permission?
321
		if ($do_change && isset($_POST['restore_files']) && in_array($file, $_POST['restore_files']))
322
		{
323
			// Use FTP if we have it.
324
			if (!empty($package_ftp))
325
			{
326
				$ftp_file = setFtpName($file);
327
				$package_ftp->chmod($ftp_file, $perms);
328
			}
329
			else
330
			{
331
				$fileFunc->elk_chmod($file, $perms);
332 4
			}
333
334
			$new_permissions = $fileFunc->filePerms($file);
335 4
			$result = $new_permissions === $perms ? 'success' : 'failure';
336
			unset($_SESSION['ftp_connection']['original_perms'][$file]);
337
		}
338
		elseif ($do_change)
339
		{
340
			$new_permissions = '';
341
			$result = 'skipped';
342
			unset($_SESSION['ftp_connection']['original_perms'][$file]);
343
		}
344
345
		// Record the results!
346
		$restore_files[] = [
347
			'path' => $file,
348
			'old_perms_raw' => $perms,
349
			'old_perms' => substr(sprintf('%o', $perms), -4),
350
			'cur_perms' => substr(sprintf('%o', $file_permissions), -4),
0 ignored issues
show
Bug introduced by
It seems like $file_permissions can also be of type false; however, parameter $values of sprintf() does only seem to accept double|integer|string, maybe add an additional type check? ( Ignorable by Annotation )

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

350
			'cur_perms' => substr(sprintf('%o', /** @scrutinizer ignore-type */ $file_permissions), -4),
Loading history...
351
			'new_perms' => isset($new_permissions) ? substr(sprintf('%o', $new_permissions), -4) : '',
352
			'result' => $result ?? '',
353
			'writable_message' => '<span class="' . (@is_writable($file) ? 'success' : 'alert') . '">' . ($fileFunc->isWritable($file) ? $txt['package_file_perms_writable'] : $txt['package_file_perms_not_writable']) . '</span>',
354
		];
355
	}
356
357
	return $restore_files;
358
}
359
360
/**
361
 * Parses the actions in package-info.xml file from packages.
362
 *
363
 * @param XmlArray $packageXML
364
 * @param bool $testing_only = true
365
 * @param string $method = 'install' ('install', 'upgrade', or 'uninstall')
366
 * @param string $previous_version = ''
367
 * @return array an array of those changes made.
368
 * @package Packages
369
 * @deprecated since 2.0 use parsePackageInfo class
370
 */
371
function parsePackageInfo(&$packageXML, $testing_only = true, $method = 'install', $previous_version = '')
372
{
373
	$parser = new PackageParser();
374
	return $parser->parsePackageInfo($packageXML, $testing_only, $method, $previous_version);
375
}
376
377
/**
378
 * Checks if version matches any of the versions in versions.
379
 *
380
 * - Supports comma separated version numbers, with or without whitespace.
381
 * - Supports lower and upper bounds. (1.0-1.2)
382
 * - Returns true if the version matched.
383
 *
384
 * @param string $versions
385
 * @param bool $reset
386
 * @param string $the_version
387
 * @return string|bool highest install value string or false
388
 * @package Packages
389
 */
390
function matchHighestPackageVersion($versions, $the_version, $reset = false)
391
{
392
	static $near_version = 0;
393
394
	if ($reset)
395
	{
396
		$near_version = 0;
397
	}
398
399
	// Normalize the $versions
400
	$versions = explode(',', str_replace(' ', '', strtolower($versions)));
401
402
	// If it is not ElkArte, let's just give up
403
	list ($the_brand,) = explode(' ', FORUM_VERSION, 2);
404
	if ($the_brand !== 'ElkArte')
405
	{
406
		return false;
407
	}
408
409
	// Loop through each version, save the highest we can find
410
	foreach ($versions as $for)
411
	{
412
		// Adjust for those wild cards
413
		if (str_contains($for, '*'))
414
		{
415
			$for = str_replace('*', '0', $for) . '-' . str_replace('*', '999', $for);
416
		}
417
418
		// If we have a range, grab the lower value, done this way so it looks normal-er to the user e.g. 1.0 vs 1.0.99
419
		if (str_contains($for, '-'))
420
		{
421
			list ($for,) = explode('-', $for);
422
		}
423
424
		// Do the compare, if the for is greater, than what we have but not greater than what we are running .....
425
		if (compareVersions($near_version, $for) === -1 && compareVersions($for, $the_version) !== 1)
426
		{
427
			$near_version = $for;
428
		}
429
	}
430
431
	return !empty($near_version) ? $near_version : false;
432
}
433
434
/**
435
 * Checks if the forum version matches any of the available versions from the package install xml.
436
 *
437
 * - Supports comma separated version numbers, with or without whitespace.
438
 * - Supports lower and upper bounds. (1.0-1.2)
439
 * - Returns true if the version matched.
440
 *
441
 * @param string $version
442
 * @param string $versions
443
 * @return bool
444
 * @package Packages
445
 */
446
function matchPackageVersion($version, $versions)
447
{
448
	// Make sure everything is lowercase and clean of spaces.
449 4
	$version = str_replace(' ', '', strtolower($version));
450
	$versions = explode(',', str_replace(' ', '', strtolower($versions)));
451
452
	// Perhaps we do accept anything?
453
	if (in_array('all', $versions))
454
	{
455
		return true;
456 2
	}
457 2
458
	// Loop through each version.
459
	foreach ($versions as $for)
460
	{
461
		// Wild card spotted?
462
		if (str_contains($for, '*'))
463 4
		{
464
			$for = str_replace('*', '0dev0', $for) . '-' . str_replace('*', '999', $for);
465
		}
466
467
		// Do we have a range?
468
		if (str_contains($for, '-'))
469
		{
470
			list ($lower, $upper) = explode('-', $for);
471
472
			// Compare the version against lower and upper bounds.
473
			if (compareVersions($version, $lower) > -1 && compareVersions($version, $upper) < 1)
474
			{
475 4
				return true;
476
			}
477
		}
478
		// Otherwise check if they are equal...
479
		elseif (compareVersions($version, $for) === 0)
480
		{
481
			return true;
482
		}
483
	}
484
485
	return false;
486
}
487
488
/**
489
 * Compares two versions and determines if one is newer, older or the same, returns
490
 *
491
 * - (-1) if version1 is lower than version2
492
 * - (0) if version1 is equal to version2
493
 * - (1) if version1 is higher than version2
494
 *
495
 * @param string $version1
496
 * @param string $version2
497
 * @return int (-1, 0, 1)
498
 * @package Packages
499
 */
500
function compareVersions($version1, $version2)
501
{
502
	static $categories;
503
504
	$versions = [];
505
	foreach ([1 => $version1, $version2] as $id => $version)
506
	{
507
		// Clean the version and extract the version parts.
508
		$clean = str_replace(' ', '', strtolower($version));
509
		preg_match('~(\d+)(?:\.(\d+|))?(?:\.)?(\d+|)(?:(alpha|beta|rc)(\d+|)(?:\.)?(\d+|))?(?:\s(dev))?(\d+|)~', $clean, $parts);
510
511
		// Build an array of parts.
512
		$versions[$id] = [
513
			'major' => !empty($parts[1]) ? (int) $parts[1] : 0,
514
			'minor' => !empty($parts[2]) ? (int) $parts[2] : 0,
515
			'patch' => !empty($parts[3]) ? (int) $parts[3] : 0,
516
			'type' => empty($parts[4]) && empty($parts[7]) ? 'stable' : (!empty($parts[7]) ? 'alpha' : $parts[4]),
517
			'type_major' => !empty($parts[6]) ? (int) $parts[5] : 0,
518
			'type_minor' => !empty($parts[6]) ? (int) $parts[6] : 0,
519
			'dev' => !empty($parts[7]),
520
		];
521
	}
522
523 4
	// Are they the same, perhaps?
524
	if ($versions[1] === $versions[2])
525 4
	{
526
		return 0;
527
	}
528 4
529
	// Get version numbering categories...
530
	if (!isset($categories))
531
	{
532
		$categories = array_keys($versions[1]);
533 4
	}
534
535
	// Loop through each category.
536
	foreach ($categories as $category)
537
	{
538
		// Is there something for us to calculate?
539
		if ($versions[1][$category] !== $versions[2][$category])
540 4
		{
541
			// Dev builds are a problematic exception.
542
			// (stable) dev < (stable) but (unstable) dev = (unstable)
543
			if ($category === 'type')
544
			{
545
				return $versions[1][$category] > $versions[2][$category] ? ($versions[1]['dev'] ? -1 : 1) : ($versions[2]['dev'] ? 1 : -1);
546 4
			}
547
548
			if ($category === 'dev')
549
			{
550
				return $versions[1]['dev'] ? ($versions[2]['type'] === 'stable' ? -1 : 0) : ($versions[1]['type'] === 'stable' ? 1 : 0);
551
			}
552
			// Otherwise a simple comparison.
553
554
			return $versions[1][$category] > $versions[2][$category] ? 1 : -1;
555
		}
556
	}
557
558
	// They are the same!
559
	return 0;
560
}
561
562
/**
563
 * Parses special identifiers out of the specified path.
564
 *
565
 * @param string $path
566
 * @return string The parsed path
567
 * @package Packages
568
 */
569
function parse_path($path)
570
{
571
	global $modSettings, $settings, $temp_path;
572
573
	if (empty($path))
574
	{
575
		return '';
576
	}
577
578
	$dirs = [
579
		'\\' => '/',
580
		'BOARDDIR' => BOARDDIR,
581
		'SOURCEDIR' => SOURCEDIR,
582
		'SUBSDIR' => SUBSDIR,
583
		'ADMINDIR' => ADMINDIR,
584
		'CONTROLLERDIR' => CONTROLLERDIR,
585
		'EXTDIR' => EXTDIR,
586
		'ADDONSDIR' => ADDONSDIR,
587
		'ELKARTEDIR' => ELKARTEDIR,
588
		'AVATARSDIR' => $modSettings['avatar_directory'],
589
		'THEMEDIR' => $settings['default_theme_dir'],
590
		'IMAGESDIR' => $settings['default_theme_dir'] . '/' . basename($settings['default_images_url']),
591
		'LANGUAGEDIR' => SOURCEDIR . '/ElkArte/Languages',
592
		'SMILEYDIR' => $modSettings['smileys_dir'],
593
	];
594
595
	// Do we parse in a package directory?
596
	if (!empty($temp_path))
597
	{
598
		$dirs['PACKAGE'] = $temp_path;
599
	}
600
601 4
	// Check if they are using some old software install paths
602
	if (str_starts_with($path, '$') && isset($dirs[strtoupper(substr($path, 1))]))
603
	{
604
		$path = strtoupper(substr($path, 1));
605
	}
606
607
	return strtr($path, $dirs);
608
}
609
610
/**
611
 * Deletes all the files in a directory, and all the files in sub directories inside it.
612
 *
613
 * What it does:
614
 *
615
 * - Requires access to delete these files.
616
 * - Recursively goes in to all sub directories looking for files to delete
617
 * - Optionally removes the directory as well, otherwise will leave an empty tree behind
618
 *
619
 * @param string $dir
620
 * @param bool $delete_dir = true
621
 * @package Packages
622
 */
623
function deltree($dir, $delete_dir = true)
624
{
625
	global $package_ftp;
626
627
	$fileFunc = FileFunctions::instance();
628
629
	if (!$fileFunc->isDir($dir))
630
	{
631
		return;
632
	}
633
634
	// Read all the files and directories in the parent directory
635
	$iterator = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);
636
	$entrynames = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::CHILD_FIRST, RecursiveIteratorIterator::CATCH_GET_CHILD);
637
638
	/** @var SplFileInfo $entryname */
639
	foreach ($entrynames as $entryname)
640
	{
641
		if ($entryname->isDir() && $delete_dir)
642
		{
643
			if (isset($package_ftp))
644
			{
645
				$ftp_file = setFtpName($entryname->getRealPath());
646
647
				if (!$fileFunc->isWritable($ftp_file . '/'))
648
				{
649
					$package_ftp->chmod($ftp_file, 0777);
650
				}
651
652
				$package_ftp->unlink($ftp_file);
653
			}
654
			else
655
			{
656
				if (!$fileFunc->isWritable($entryname))
657
				{
658
					$fileFunc->chmod($entryname->getRealPath());
659
				}
660
661
				@rmdir($entryname->getRealPath());
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for rmdir(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

661
				/** @scrutinizer ignore-unhandled */ @rmdir($entryname->getRealPath());

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...
662
			}
663
		}
664
		// A file, delete it by any means necessary
665
		else
666
		{
667
			if (isset($package_ftp))
668
			{
669
				// Here, 755 doesn't really matter since we're deleting it anyway.
670
				$ftp_file = setFtpName($entryname->getPathname());
671
672
				if (!$fileFunc->isWritable($ftp_file))
673
				{
674
					$package_ftp->chmod($ftp_file, 0777);
675
				}
676
677
				$package_ftp->unlink($ftp_file);
678
			}
679
			else
680
			{
681
				if (!$entryname->isWritable())
682
				{
683
					$fileFunc->chmod($entryname->getRealPath());
684
				}
685
686
				$fileFunc->delete($entryname->getRealPath());
687
			}
688
		}
689
	}
690
691
	// Finish off with the directory itself
692
	if ($delete_dir)
693
	{
694
		if (isset($package_ftp))
695
		{
696
			$ftp_file = setFtpName(realpath($dir));
697
			$package_ftp->unlink($ftp_file);
698
		}
699
		else
700
		{
701
			@rmdir(realpath($dir));
702
		}
703
	}
704
}
705
706
/**
707
 * Creates the specified tree structure with a mode that permits write access.
708
 *
709
 * - Creates every directory in path until it finds one that already exists.
710
 *
711
 * @param string $strPath
712
 * @param bool $mode true attempts to make a writable tree
713
 * @return bool true if successful, false otherwise
714
 * @package Packages
715
 */
716
function mktree($strPath, $mode = true)
717
{
718
	global $package_ftp;
719
720
	$fileFunc = FileFunctions::instance();
721
722
	// If already a directory
723
	if ($fileFunc->isDir($strPath))
724
	{
725
		// Not writable, try to make it so with FTP or not
726
		if (!$fileFunc->isWritable($strPath) && $mode !== false)
727
		{
728
			if (isset($package_ftp))
729
			{
730
				$package_ftp->ftp_chmod(setFtpName($strPath), [0755, 0775, 0777]);
731
			}
732
			else
733
			{
734
				$fileFunc->chmod($strPath);
735
			}
736
		}
737
738
		// See if we can open it for access, return the result
739
		return test_access($strPath);
740
	}
741
742
	// Is this an invalid path and/or we can't make the directory?
743
	if ($strPath === dirname($strPath) || !mktree(dirname($strPath), $mode))
744
	{
745
		return false;
746
	}
747
748
	// Is the dir writable and do we have permission to attempt to make it so
749
	if (!$fileFunc->isWritable(dirname($strPath)) && $mode !== false)
750
	{
751
		if (isset($package_ftp))
752
		{
753
754
			$package_ftp->ftp_chmod(dirname(setFtpName($strPath)), [0755, 0775, 0777]);
755
		}
756
		else
757
		{
758
			$fileFunc->chmod(dirname($strPath));
759
		}
760
	}
761
762
	// Can't change the mode so just return the current availability
763
	if ($mode === false)
764
	{
765
		return test_access($strPath);
766
	}
767
	// Let FTP take care of this directory creation
768
	if (isset($package_ftp, $_SESSION['ftp_connection']))
769
	{
770
		return $package_ftp->create_dir(setFtpName($strPath));
771
	}
772
	// Only one choice left and that is to try and make a directory with PHP
773
774
	try
775
	{
776
		return $fileFunc->createDirectory($strPath, false);
777
	}
778
	catch (Exception $e)
779
	{
780
		return false;
781
	}
782
}
783
784
/**
785
 * Determines if a directory is writable
786
 *
787
 * @param string $strPath
788
 * @return bool
789
 */
790
function dirTest($strPath)
791
{
792
	$fileFunc = FileFunctions::instance();
793
794
	// If it is already a directory
795
	if ($fileFunc->isDir($strPath))
796
	{
797
		// See if we can open it for access, return the result
798
		return test_access($strPath);
799
	}
800
801
	// Is this an invalid path ?
802
	if ($strPath === dirname($strPath) || !dirTest(dirname($strPath)))
803
	{
804
		return false;
805
	}
806
807
	// Return the current availability
808
	return test_access(dirname($strPath));
809
}
810
811
/**
812
 * Copies one directory structure over to another.
813
 *
814
 * - Requires the destination to be writable.
815
 *
816
 * @param string $source
817
 * @param string $destination
818
 * @package Packages
819
 */
820
function copytree($source, $destination)
821
{
822
	global $package_ftp;
823
824
	$fileFunc = FileFunctions::instance();
825
826
	// The destination must exist and be writable
827
	if (!$fileFunc->isDIr($destination) || !$fileFunc->isWritable($destination))
828
	{
829
		try
830
		{
831
			$fileFunc->createDirectory($destination, false);
832
		}
833
		catch (Exception $e)
834
		{
835
			return;
836
		}
837
	}
838
839
	$current_dir = opendir($source);
840
	if ($current_dir === false)
841
	{
842
		return;
843
	}
844
845
	// Copy the files over by whatever means we have enabled
846
	while (($entryname = readdir($current_dir)))
847
	{
848
		if (in_array($entryname, ['.', '..']))
849
		{
850
			continue;
851
		}
852
853
		if (isset($package_ftp))
854
		{
855
			$ftp_file = setFtpName($destination . '/' . $entryname);
856
		}
857
858
		if (!$fileFunc->isDir($source . '/' . $entryname))
859
		{
860
			if (isset($package_ftp) && !$fileFunc->fileExists($destination . '/' . $entryname))
861
			{
862
				$package_ftp->create_file($ftp_file);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $ftp_file does not seem to be defined for all execution paths leading up to this point.
Loading history...
863
			}
864
			elseif (!$fileFunc->fileExists($destination . '/' . $entryname))
865
			{
866
				@touch($destination . '/' . $entryname);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for touch(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

866
				/** @scrutinizer ignore-unhandled */ @touch($destination . '/' . $entryname);

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...
867
			}
868
		}
869
870
		$packageChmod = new PackageChmod();
871
		$packageChmod->pkgChmod($destination . '/' . $entryname);
872
873
		if ($fileFunc->isDir($source . '/' . $entryname))
874
		{
875
			copytree($source . '/' . $entryname, $destination . '/' . $entryname);
876
		}
877
		elseif ($fileFunc->fileExists($destination . '/' . $entryname))
878
		{
879
			package_put_contents($destination . '/' . $entryname, package_get_contents($source . '/' . $entryname));
880
		}
881
		else
882
		{
883
			copy($source . '/' . $entryname, $destination . '/' . $entryname);
884
		}
885
	}
886
887
	closedir($current_dir);
888
}
889
890
/**
891
 * Parses a xml-style modification file (file).
892
 *
893
 * @param string $file
894
 * @param bool $testing = true means modifications shouldn't actually be saved.
895
 * @param bool $undo = false specifies that the modifications the file requests should be undone; this doesn't work with everything (regular expressions.)
896
 * @param array $theme_paths = array()
897
 * @return array an array of those changes made.
898
 * @package Packages
899
 */
900
function parseModification($file, $testing = true, $undo = false, $theme_paths = [])
901
{
902
	global $txt, $modSettings;
903
904
	detectServer()->setTimeLimit(600);
905
906
	$xml = new XmlArray(strtr($file, ["\r" => '']));
907
	$actions = [];
908
	$everything_found = true;
909
910
	if (!$xml->exists('modification') || !$xml->exists('modification/file'))
911
	{
912
		$actions[] = [
913
			'type' => 'error',
914
			'filename' => '-',
915
			'debug' => $txt['package_modification_malformed']
916
		];
917
918
		return $actions;
919
	}
920
921
	// Get the XML data.
922
	$files = $xml->set('modification/file');
923
924
	// Use this for holding all the template changes in this mod.
925
	$template_changes = [];
926
927
	// This is needed to hold the long paths, as they can vary...
928
	$long_changes = [];
929
930
	// First, we need to build the list of all the files likely to get changed.
931
	foreach ($files as $file)
0 ignored issues
show
introduced by
$file is overwriting one of the parameters of this function.
Loading history...
932
	{
933
		// What is the filename we're currently on?
934
		$filename = parse_path(trim($file->fetch('@name')));
935
936
		// Now, we need to work out whether this is even a template file...
937
		foreach ($theme_paths as $id => $theme)
938
		{
939
			// If this filename is relative, if so take a guess at what it should be.
940
			$real_filename = $filename;
941 2
			if (str_starts_with($filename, 'themes'))
942
			{
943
				$real_filename = BOARDDIR . '/' . $filename;
944 2
			}
945
946
			if (str_starts_with($real_filename, $theme['theme_dir']))
947
			{
948
				$template_changes[$id][] = substr($real_filename, strlen($theme['theme_dir']) + 1);
949
				$long_changes[$id][] = $filename;
950 2
			}
951 2
		}
952
	}
953
954 2
	// Custom themes to add.
955
	$custom_themes_add = [];
956
957
	// If we have some template changes, we need to build a master link of what new ones are required for the custom themes.
958
	if (!empty($template_changes[1]))
959
	{
960 2
		foreach ($theme_paths as $id => $theme)
961
		{
962 2
			// Default is getting done anyway, so no need for involvement here.
963 2
			if ($id == 1)
964
			{
965
				continue;
966 2
			}
967
968 2
			// For every template, do we want it? Yea, no, maybe?
969
			foreach ($template_changes[1] as $index => $template_file)
970
			{
971
				// What, it exists and we haven't already got it?! Lordy, get it in!
972 2
				if (file_exists($theme['theme_dir'] . '/' . $template_file) && (!isset($template_changes[$id]) || !in_array($template_file, $template_changes[$id])))
973 2
				{
974
					// Now let's add it to the "todo" list.
975
					$custom_themes_add[$long_changes[1][$index]][$id] = $theme['theme_dir'] . '/' . $template_file;
976 2
				}
977
			}
978
		}
979 2
	}
980
981 2
	foreach ($files as $file)
0 ignored issues
show
introduced by
$file is overwriting one of the parameters of this function.
Loading history...
982
	{
983
		// This is the actual file referred to in the XML document...
984
		$files_to_change = [
985
			1 => parse_path(trim($file->fetch('@name'))),
986 2
		];
987
988
		// Sometimes though, we have some additional files for other themes, if we have add them to the mix.
989
		if (isset($custom_themes_add[$files_to_change[1]]))
990
		{
991
			$files_to_change += $custom_themes_add[$files_to_change[1]];
992
		}
993
994
		// Now, loop through all the files we're changing, and, well, change them ;)
995
		foreach ($files_to_change as $theme => $working_file)
996 2
		{
997 2
			if ($working_file[0] !== '/' && $working_file[1] !== ':')
998
			{
999
				trigger_error('parseModification(): The filename \'' . $working_file . '\' is not a full path!', E_USER_WARNING);
1000
1001 2
				$working_file = BOARDDIR . '/' . $working_file;
1002
			}
1003
1004
			// Doesn't exist - give an error or what?
1005
			if (!file_exists($working_file) && (!$file->exists('@error') || !in_array(trim($file->fetch('@error')), ['ignore', 'skip'])))
1006
			{
1007 2
				$actions[] = [
1008 2
					'type' => 'missing',
1009
					'filename' => $working_file,
1010 2
					'debug' => $txt['package_modification_missing']
1011 2
				];
1012
1013 2
				$everything_found = false;
1014 2
				continue;
1015 2
			}
1016
1017
			// Skip the file if it doesn't exist.
1018 2
			if (!file_exists($working_file) && $file->exists('@error') && trim($file->fetch('@error')) === 'skip')
1019
			{
1020 2
				$actions[] = [
1021
					'type' => 'skipping',
1022 2
					'filename' => $working_file,
1023
				];
1024 2
				continue;
1025
			}
1026 2
1027
			// Okay, we're creating this file then...?
1028
			if (!file_exists($working_file))
1029
			{
1030 2
				$working_data = '';
1031
			}
1032 2
			// Phew, it exists!  Load 'er up!
1033 2
			else
1034
			{
1035
				$working_data = str_replace("\r", '', package_get_contents($working_file));
1036
			}
1037
1038
			$actions[] = [
1039
				'type' => 'opened',
1040
				'filename' => $working_file
1041
			];
1042
1043
			$operations = $file->exists('operation') ? $file->set('operation') : [];
1044
			foreach ($operations as $operation)
1045
			{
1046
				// Convert operation to an array.
1047
				$actual_operation = [
1048
					'searches' => [],
1049
					'error' => $operation->exists('@error') && in_array(trim($operation->fetch('@error')), ['ignore', 'fatal', 'required']) ? trim($operation->fetch('@error')) : 'fatal',
1050
				];
1051
1052
				// The 'add' parameter is used for all searches in this operation.
1053
				$add = $operation->exists('add') ? $operation->fetch('add') : '';
1054 2
1055
				// Grab all search items of this operation (in most cases just 1).
1056
				$searches = $operation->set('search');
1057
				foreach ($searches as $i => $search)
1058
				{
1059
					$actual_operation['searches'][] = [
1060
						'position' => $search->exists('@position') && in_array(trim($search->fetch('@position')), ['before', 'after', 'replace', 'end']) ? trim($search->fetch('@position')) : 'replace',
1061
						'is_reg_exp' => $search->exists('@regexp') && trim($search->fetch('@regexp')) === 'true',
1062 2
						'loose_whitespace' => $search->exists('@whitespace') && trim($search->fetch('@whitespace')) === 'loose',
1063
						'search' => $search->fetch('.'),
1064
						'add' => $add,
1065
						'preg_search' => '',
1066
						'preg_replace' => '',
1067 2
					];
1068
				}
1069 2
1070 2
				// At least one search should be defined.
1071 2
				if (empty($actual_operation['searches']))
1072
				{
1073
					$actions[] = [
1074
						'type' => 'failure',
1075 2
						'filename' => $working_file,
1076
						'search' => $search['search'],
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $search does not seem to be defined for all execution paths leading up to this point.
Loading history...
1077
						'is_custom' => $theme > 1 ? $theme : 0,
1078 2
					];
1079 2
1080 2
					// Skip to the next operation.
1081 2
					continue;
1082 2
				}
1083 2
1084 2
				// Reverse the operations in case of undoing stuff.
1085 2
				if ($undo)
1086 2
				{
1087
					foreach ($actual_operation['searches'] as $i => $search)
1088
					{
1089 2
						// Reverse modification of regular expressions are not allowed.
1090
						if ($search['is_reg_exp'])
1091 2
						{
1092
							if ($actual_operation['error'] === 'fatal')
1093 2
							{
1094 2
								$actions[] = [
1095 2
									'type' => 'failure',
1096 2
									'filename' => $working_file,
1097 2
									'search' => $search['search'],
1098 2
									'is_custom' => $theme > 1 ? $theme : 0,
1099 2
								];
1100
							}
1101 2
1102
							// Continue to the next operation.
1103 2
							continue 2;
1104
						}
1105
1106
						// The replacement is now the search subject...
1107
						if ($search['position'] === 'replace' || $search['position'] === 'end')
1108
						{
1109
							$actual_operation['searches'][$i]['search'] = $search['add'];
1110
						}
1111
						else
1112
						{
1113
							// Reversing a before/after modification becomes a replacement.
1114
							$actual_operation['searches'][$i]['position'] = 'replace';
1115
1116
							if ($search['position'] === 'before')
1117
							{
1118
								$actual_operation['searches'][$i]['search'] .= $search['add'];
1119
							}
1120
							elseif ($search['position'] === 'after')
1121
							{
1122
								$actual_operation['searches'][$i]['search'] = $search['add'] . $search['search'];
1123
							}
1124
						}
1125
1126 2
						// ...and the search subject is now the replacement.
1127
						$actual_operation['searches'][$i]['add'] = $search['search'];
1128
					}
1129
				}
1130
1131
				// Sort the search list so the replaces come before the add before/after's.
1132
				if (count($actual_operation['searches']) !== 1)
1133
				{
1134
					$replacements = [];
1135
1136 2
					foreach ($actual_operation['searches'] as $i => $search)
1137
					{
1138
						if ($search['position'] === 'replace')
1139
						{
1140
							$replacements[] = $search;
1141
							unset($actual_operation['searches'][$i]);
1142 1
						}
1143
					}
1144 2
					$actual_operation['searches'] = array_merge($replacements, $actual_operation['searches']);
1145
				}
1146 2
1147 2
				// Create regular expression replacements from each search.
1148 2
				foreach ($actual_operation['searches'] as $i => $search)
1149
				{
1150
					// Not much needed if the search subject is already a regexp.
1151
					if ($search['is_reg_exp'])
1152 2
					{
1153
						$actual_operation['searches'][$i]['preg_search'] = $search['search'];
1154 2
					}
1155 2
					else
1156
					{
1157
						// Make the search subject fit into a regular expression.
1158
						$actual_operation['searches'][$i]['preg_search'] = preg_quote($search['search'], '~');
1159
1160
						// Using 'loose', a random amount of tabs and spaces may be used.
1161
						if ($search['loose_whitespace'])
1162
						{
1163
							$actual_operation['searches'][$i]['preg_search'] = preg_replace('~[ \t]+~', '[ \t]+', $actual_operation['searches'][$i]['preg_search']);
1164 2
						}
1165
					}
1166 2
1167
					// Shuzzup.  This is done so we can safely use a regular expression. ($0 is bad!!)
1168
					$actual_operation['searches'][$i]['preg_replace'] = strtr($search['add'], ['$' => '[$PACKAGE1$]', '\\' => '[$PACKAGE2$]']);
1169
1170
					// Before, so the replacement comes after the search subject :P
1171
					if ($search['position'] === 'before')
1172 2
					{
1173
						$actual_operation['searches'][$i]['preg_search'] = '(' . $actual_operation['searches'][$i]['preg_search'] . ')';
1174
						$actual_operation['searches'][$i]['preg_replace'] = '$1' . $actual_operation['searches'][$i]['preg_replace'];
1175
					}
1176
1177 2
					// After, after what?
1178
					elseif ($search['position'] === 'after')
1179
					{
1180
						$actual_operation['searches'][$i]['preg_search'] = '(' . $actual_operation['searches'][$i]['preg_search'] . ')';
1181
						$actual_operation['searches'][$i]['preg_replace'] .= '$1';
1182
					}
1183
1184
					// Position the replacement at the end of the file (or just before the closing PHP tags).
1185
					elseif ($search['position'] === 'end')
1186
					{
1187
						if ($undo)
1188
						{
1189
							$actual_operation['searches'][$i]['preg_replace'] = '';
1190
						}
1191
						else
1192
						{
1193
							$actual_operation['searches'][$i]['preg_search'] = '(\\n\\?\\>)?$';
1194 2
							$actual_operation['searches'][$i]['preg_replace'] .= '$1';
1195
						}
1196
					}
1197
1198
					// Testing 1, 2, 3...
1199
					$failed = preg_match('~' . $actual_operation['searches'][$i]['preg_search'] . '~s', $working_data) === 0;
1200
1201
					// Nope, search pattern not found, or Found, but in this case, that means failure!
1202
					if (($failed && $actual_operation['error'] === 'fatal')
1203
						|| (!$failed && $actual_operation['error'] === 'required'))
1204
					{
1205
						$actions[] = [
1206
							'type' => 'failure',
1207
							'filename' => $working_file,
1208
							'search' => $actual_operation['searches'][$i]['preg_search'],
1209
							'search_original' => $actual_operation['searches'][$i]['search'],
1210
							'replace_original' => $actual_operation['searches'][$i]['add'],
1211
							'position' => $search['position'],
1212
							'is_custom' => $theme > 1 ? $theme : 0,
1213
							'failed' => $failed,
1214
						];
1215
1216
						$everything_found = false;
1217
						continue;
1218
					}
1219 2
1220
					// Replace it into nothing? That's not an option...unless it's an undoing end.
1221
					if ($search['add'] === '' && ($search['position'] !== 'end' || !$undo))
1222
					{
1223
						continue;
1224
					}
1225
1226
					// Finally, we're doing some replacements.
1227
					$working_data = preg_replace('~' . $actual_operation['searches'][$i]['preg_search'] . '~s', $actual_operation['searches'][$i]['preg_replace'], $working_data, 1);
1228
1229
					$actions[] = [
1230
						'type' => 'replace',
1231
						'filename' => $working_file,
1232
						'search' => $actual_operation['searches'][$i]['preg_search'],
1233
						'replace' => $actual_operation['searches'][$i]['preg_replace'],
1234
						'search_original' => $actual_operation['searches'][$i]['search'],
1235 2
						'replace_original' => $actual_operation['searches'][$i]['add'],
1236
						'position' => $search['position'],
1237 2
						'failed' => $failed,
1238
						'ignore_failure' => $failed && $actual_operation['error'] === 'ignore',
1239
						'is_custom' => $theme > 1 ? $theme : 0,
1240
					];
1241
				}
1242 2
			}
1243
1244
			// Fix any little helper symbols ;).
1245
			$working_data = strtr($working_data, ['[$PACKAGE1$]' => '$', '[$PACKAGE2$]' => '\\']);
1246
1247
			$packageChmod = new PackageChmod();
1248
			$packageChmod->pkgChmod($working_file);
1249
1250
			if ((file_exists($working_file) && !is_writable($working_file)) || (!file_exists($working_file) && !is_writable(dirname($working_file))))
1251
			{
1252
				$actions[] = [
1253
					'type' => 'chmod',
1254
					'filename' => $working_file
1255
				];
1256 2
			}
1257
1258 2
			if (basename($working_file) === 'Settings_bak.php')
1259 2
			{
1260 2
				continue;
1261
			}
1262
1263
			if (!$testing && !empty($modSettings['package_make_backups']) && file_exists($working_file))
1264
			{
1265
				// No, no, not Settings.php!
1266
				if (basename($working_file) === 'Settings.php')
1267
				{
1268
					@copy($working_file, dirname($working_file) . '/Settings_bak.php');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for copy(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

1268
					/** @scrutinizer ignore-unhandled */ @copy($working_file, dirname($working_file) . '/Settings_bak.php');

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...
1269
				}
1270
				else
1271
				{
1272
					@copy($working_file, $working_file . '~');
1273
				}
1274
			}
1275
1276
			// Always call this, even if in testing, because it won't really be written in testing mode.
1277
			package_put_contents($working_file, $working_data, $testing);
1278
1279
			$actions[] = [
1280
				'type' => 'saved',
1281
				'filename' => $working_file,
1282
				'is_custom' => $theme > 1 ? $theme : 0,
1283
			];
1284
		}
1285
	}
1286
1287
	$actions[] = [
1288
		'type' => 'result',
1289
		'status' => $everything_found
1290
	];
1291
1292
	return $actions;
1293
}
1294
1295
/**
1296
 * Get the physical contents of a packages file
1297
 *
1298
 * @param string $filename
1299
 * @return string
1300
 * @package Packages
1301
 */
1302
function package_get_contents($filename)
1303
{
1304 2
	global $package_cache, $modSettings;
1305
1306
	if (!isset($package_cache))
1307
	{
1308
		$mem_check = detectServer()->setMemoryLimit('128M');
1309
1310
		// Windows doesn't seem to care about the memory_limit.
1311
		if (!empty($modSettings['package_disable_cache']) || $mem_check || str_contains(PHP_OS_FAMILY, 'Win'))
1312
		{
1313
			$package_cache = [];
1314 1
		}
1315
		else
1316
		{
1317
			$package_cache = false;
1318
		}
1319 2
	}
1320
1321
	if (str_contains($filename, 'packages/') || $package_cache === false || !isset($package_cache[$filename]))
1322
	{
1323
		return file_get_contents($filename);
1324
	}
1325
1326
	return $package_cache[$filename];
1327
}
1328
1329
/**
1330
 * Writes data to a file, almost exactly like the file_put_contents() function.
1331
 *
1332
 * - uses FTP to create/chmod the file when necessary and available.
1333
 * - uses text mode for text mode file extensions.
1334 2
 * - returns the number of bytes written.
1335
 *
1336 2
 * @param string $filename
1337
 * @param string $data
1338
 * @param bool $testing
1339
 * @return int|bool
1340
 * @package Packages
1341
 */
1342
function package_put_contents($filename, $data, $testing = false)
1343
{
1344
	/** @var $package_ftp FtpConnection */
1345
	global $package_ftp, $package_cache, $modSettings;
1346
	static $text_filetypes = ['php', 'txt', 'js', 'css', 'vbs', 'html', 'htm', 'log', 'xml', 'csv'];
1347
1348
	if (!isset($package_cache))
1349
	{
1350
		// Try to increase the memory limit - we don't want to run out of ram!
1351
		$mem_check = detectServer()->setMemoryLimit('128M');
1352
1353
		if (!empty($modSettings['package_disable_cache']) || $mem_check || str_contains(PHP_OS_FAMILY, 'Win'))
1354
		{
1355
			$package_cache = [];
1356
		}
1357
		else
1358
		{
1359
			$package_cache = false;
1360
		}
1361
	}
1362
1363
	$fileFunc = FileFunctions::instance();
1364
	if (isset($package_ftp, $_SESSION['ftp_connection']))
1365
	{
1366
		$ftp_file = setFtpName($filename);
1367
	}
1368
1369
	@touch($filename);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for touch(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

1369
	/** @scrutinizer ignore-unhandled */ @touch($filename);

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...
1370
	if (isset($package_ftp) && !$fileFunc->fileExists($filename))
1371
	{
1372
		$package_ftp->create_file($ftp_file);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $ftp_file does not seem to be defined for all execution paths leading up to this point.
Loading history...
1373
	}
1374
1375
	$packageChmod = new PackageChmod();
1376
	$packageChmod->pkgChmod($filename);
1377
1378
	if (!$testing && (str_contains($filename, 'packages/') || $package_cache === false))
1379
	{
1380
		$path_ext = pathinfo($filename, PATHINFO_EXTENSION);
1381
		$fp = @fopen($filename, in_array($path_ext, $text_filetypes) ? 'w' : 'wb');
1382
1383
		// We should show an error message or attempt a rollback, no?
1384
		if (!$fp)
0 ignored issues
show
introduced by
$fp is of type false|resource, thus it always evaluated to false.
Loading history...
1385
		{
1386
			return false;
1387
		}
1388
1389
		fwrite($fp, $data);
1390
		fclose($fp);
1391
	}
1392
	elseif (str_contains($filename, 'packages/') || $package_cache === false)
1393
	{
1394
		return strlen($data);
1395
	}
1396
	else
1397
	{
1398
		$package_cache[$filename] = $data;
1399
1400
		// Permission denied, eh?
1401
		$fp = @fopen($filename, 'rb+');
1402
		if (!$fp)
0 ignored issues
show
introduced by
$fp is of type false|resource, thus it always evaluated to false.
Loading history...
1403
		{
1404
			return false;
1405
		}
1406
		fclose($fp);
1407
	}
1408
1409
	return strlen($data);
1410
}
1411
1412
/**
1413
 * Clears (removes the files) the current package cache (temp directory)
1414
 *
1415
 * @param bool $trash
1416
 * @package Packages
1417
 */
1418
function package_flush_cache($trash = false)
1419
{
1420
	global $package_ftp, $package_cache;
1421
	static $text_filetypes = ['php', 'txt', 'js', 'css', 'vbs', 'html', 'htm', 'log', 'xml', 'csv'];
1422
1423
	if (empty($package_cache))
1424
	{
1425
		return;
1426
	}
1427
1428
	$fileFunc = FileFunctions::instance();
1429
1430
	// First, let's check permissions!
1431
	foreach ($package_cache as $filename => $data)
1432
	{
1433
		if (isset($package_ftp))
1434
		{
1435
			$path_parts = pathinfo($filename, PATHINFO_DIRNAME);
1436
			$path_parts['dirname'] = str_replace($_SESSION['ftp_connection']['root'], '', $path_parts['dirname']);
1437
			$ftp_file = $path_parts['dirname'] . '/' . $path_parts['filename'];
1438
		}
1439
1440
		if (!$fileFunc->fileExists($filename))
1441
		{
1442
			if (isset($package_ftp))
1443
			{
1444
				$package_ftp->create_file($ftp_file);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $ftp_file does not seem to be defined for all execution paths leading up to this point.
Loading history...
1445
			}
1446
			else
1447
			{
1448
				@touch($filename);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for touch(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

1448
				/** @scrutinizer ignore-unhandled */ @touch($filename);

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...
1449
			}
1450
		}
1451
1452
		$packageChmod = new PackageChmod();
1453
		$result = $packageChmod->pkgChmod($filename);
1454
1455
		// If we are not doing our test pass, then lets do a full write check
1456
		if (!$trash && !$fileFunc->isDir($filename))
1457
		{
1458
			// Acid test, can we really open this file for writing?
1459
			$fp = ($result) ? fopen($filename, 'rb+') : $result;
1460
			if (!$fp)
1461
			{
1462
				// We should have package_chmod()'d them before, no?!
1463
				trigger_error('package_flush_cache(): (' . $filename . ') file is still not writable', E_USER_WARNING);
1464
1465
				return;
1466
			}
1467
			fclose($fp);
1468
		}
1469
	}
1470
1471
	if ($trash)
1472
	{
1473
		$package_cache = [];
1474
1475
		return;
1476
	}
1477
1478
	foreach ($package_cache as $filename => $data)
1479
	{
1480
		if (!$fileFunc->isDir($filename))
1481
		{
1482
			$path_ext = pathinfo($filename, PATHINFO_EXTENSION);
1483
			$fp = fopen($filename, in_array($path_ext, $text_filetypes) ? 'w' : 'wb');
1484
			fwrite($fp, $data);
1485
			fclose($fp);
1486
		}
1487
	}
1488
1489
	$package_cache = [];
1490
}
1491
1492
/**
1493
 * Try to make a file writable.
1494
 *
1495
 * @param string $filename
1496
 * @param bool $track_change = false
1497
 * @return bool True if it worked, false if it didn't
1498
 * @package Packages
1499
 * @deprecated since 2.0 use PackageChmod class
1500
 */
1501
function package_chmod($filename, $track_change = false)
1502
{
1503
	$packageChmod = new PackageChmod();
1504
	return $packageChmod->pkgChmod($filename, $track_change);
1505
}
1506
1507
/**
1508
 * Creates a site backup before installing a package just in case things don't go
1509
 * as planned.
1510
 *
1511
 * @param string $id
1512
 *
1513
 * @return bool
1514
 * @package Packages
1515
 *
1516
 */
1517
function package_create_backup($id = 'backup')
1518
{
1519
	$db = database();
1520
	$files = new ArrayIterator();
1521
	$use_relative_paths = empty($_REQUEST['use_full_paths']);
1522
	$fileFunc = FileFunctions::instance();
1523
1524
	// The files that reside outside of sources, in the base, we add manually
1525
	$base_files = ['index.php', 'SSI.php', 'subscriptions.php',	'email_imap_cron.php', 'emailpost.php', 'emailtopic.php'];
1526
	foreach ($base_files as $file)
1527
	{
1528
		if ($fileFunc->fileExists(BOARDDIR . '/' . $file))
1529
		{
1530
			$files[$use_relative_paths ? $file : realpath(BOARDDIR . '/' . $file)] = BOARDDIR . '/' . $file;
1531
		}
1532
	}
1533
1534
	// Root directory where most of our files reside
1535
	$dirs = [
1536
		SOURCEDIR => $use_relative_paths ? 'sources/' : str_replace('\\', '/', SOURCEDIR . '/')
1537
	];
1538
1539
	// Find all installed theme directories
1540
	$db->fetchQuery('
1541
		SELECT 
1542
			value
1543
		FROM {db_prefix}themes
1544
		WHERE id_member = {int:no_member}
1545
			AND variable = {string:theme_dir}',
1546
		[
1547
			'no_member' => 0,
1548
			'theme_dir' => 'theme_dir',
1549 32
		]
1550 32
	)->fetch_callback(
1551
		function ($row) use (&$dirs, $use_relative_paths) {
1552
			$dirs[$row['value']] = $use_relative_paths ? 'themes/' . basename($row['value']) . '/' : str_replace('\\', '/', $row['value'] . '/');
1553 32
		}
1554
	);
1555
1556
	try
1557
	{
1558
		foreach ($dirs as $dir => $dest)
1559 32
		{
1560
			$iter = new RecursiveIteratorIterator(
1561
				new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
1562 32
				RecursiveIteratorIterator::CHILD_FIRST,
1563
				RecursiveIteratorIterator::CATCH_GET_CHILD // Ignore "Permission denied"
1564
			);
1565
1566
			foreach ($iter as $entry => $dir)
0 ignored issues
show
Comprehensibility Bug introduced by
$dir is overwriting a variable from outer foreach loop.
Loading history...
1567
			{
1568 32
				if ($dir->isDir())
1569
				{
1570 32
					continue;
1571
				}
1572
1573 32
				if (preg_match('~^(\.{1,2}|CVS|backup.*|help|images|.*\~)$~', $entry) != 0)
0 ignored issues
show
Bug introduced by
It seems like $entry can also be of type null and true; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1573
				if (preg_match('~^(\.{1,2}|CVS|backup.*|help|images|.*\~)$~', /** @scrutinizer ignore-type */ $entry) != 0)
Loading history...
1574
				{
1575 32
					continue;
1576
				}
1577
1578
				$files[$use_relative_paths ? str_replace(realpath(BOARDDIR), '', $entry) : $entry] = $entry;
1579
			}
1580
		}
1581 1
1582
		// Make sure we have a backup directory and its writable
1583
		if (!$fileFunc->fileExists(BOARDDIR . '/packages/backups'))
1584
		{
1585 2
			$fileFunc->createDirectory(BOARDDIR . '/packages/backups');
1586
		}
1587
1588
		if (!$fileFunc->isWritable(BOARDDIR . '/packages/backups'))
1589
		{
1590
			$packageChmod = new PackageChmod();
1591
			$packageChmod->pkgChmod(BOARDDIR . '/packages/backups');
1592
		}
1593
1594
		// Name the output file, yyyy-mm-dd_before_package_name.tar.gz
1595
		$output_file = BOARDDIR . '/packages/backups/' . Util::strftime('%Y-%m-%d_') . preg_replace('~[$\\\\/:<>|?*"\']~', '', $id);
1596
		$output_ext = '.tar';
1597
1598
		if ($fileFunc->fileExists($output_file . $output_ext . '.gz'))
1599
		{
1600
			$i = 2;
1601
			while ($fileFunc->fileExists($output_file . '_' . $i . $output_ext . '.gz'))
1602 32
			{
1603
				$i++;
1604 32
			}
1605 32
			$output_file .= '_' . $i . $output_ext;
1606
		}
1607
		else
1608 32
		{
1609 32
			$output_file .= $output_ext;
1610
		}
1611
1612 32
		// Buy some more time, so we have enough to create this archive
1613 32
		detectServer()->setTimeLimit(300);
1614 32
1615 32
		$phar = new PharData($output_file);
1616 32
		$phar->buildFromIterator($files);
1617 32
		$phar->compress(Phar::GZ);
1618 32
1619 32
		/*
1620
		 * Destroying the local var tells PharData to close its internal
1621
		 * file pointer, enabling us to delete the uncompressed tarball.
1622
		 */
1623
		unset($phar);
1624 32
		unlink($output_file);
1625
	}
1626 32
	catch (Exception $e)
1627
	{
1628
		\ElkArte\Errors\Errors::instance()->log_error($e->getMessage(), 'backup');
0 ignored issues
show
Bug introduced by
The type ElkArte\Errors\Errors was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
1629
1630 32
		return false;
1631
	}
1632
1633
	return true;
1634
}
1635
1636 32
/**
1637
 * Get the contents of a URL, irrespective of allow_url_fopen.
1638
 *
1639 32
 * - reads the contents of http or ftp addresses and returns the page in a string
1640
 * - will accept up to (3) redirections (redirection_level in the function call is private)
1641
 * - if post_data is supplied, the value and length is posted to the given url as form data
1642
 * - URL must be supplied in lowercase
1643 32
 *
1644
 * @param string $url
1645
 * @param string $post_data = ''
1646
 * @param bool $keep_alive = false
1647 32
 * @param int $redirection_level = 3
1648
 * @return false|string
1649
 * @package Packages
1650
 */
1651
function fetch_web_data($url, $post_data = '', $keep_alive = false, $redirection_level = 3)
1652
{
1653
	global $webmaster_email;
1654 32
1655
	preg_match('~^(http|ftp)(s)?://([^/:]+)(:(\d+))?(.+)$~', $url, $match);
1656
	$data = '';
1657
1658
	// An FTP url. We should try connecting and RETRieving it...
1659
	if (empty($match[1]))
1660
	{
1661
		return false;
1662
	}
1663
1664
	if ($match[1] === 'ftp')
1665
	{
1666
		// Establish a connection and attempt to enable passive mode.
1667
		$ftp = new FtpConnection(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? 21 : $match[5], 'anonymous', $webmaster_email);
1668
		if ($ftp->error !== false || !$ftp->passive())
1669
		{
1670
			return false;
1671
		}
1672 2
1673
		// I want that one *points*!
1674 2
		fwrite($ftp->connection, 'RETR ' . $match[6] . "\r\n");
1675
1676
		// Since passive mode worked (or we would have returned already!) open the connection.
1677
		$fp = @fsockopen($ftp->pasv['ip'], $ftp->pasv['port'], $err, $err, 5);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $err seems to be never defined.
Loading history...
1678
		if (!$fp)
0 ignored issues
show
introduced by
$fp is of type false|resource, thus it always evaluated to false.
Loading history...
1679
		{
1680 2
			return false;
1681
		}
1682
1683
		// The server should now say something in acknowledgement.
1684
		$ftp->check_response(150);
1685
1686
		while (!feof($fp))
1687
		{
1688 2
			$data .= fread($fp, 4096);
1689 2
		}
1690 2
		fclose($fp);
1691 2
1692 2
		// All done, right?  Good.
1693
		$ftp->check_response(226);
1694
		$ftp->close();
1695
	}
1696 2
	// More likely a standard HTTP URL, first try to use cURL if available
1697
	elseif ($match[1] === 'http')
1698 2
	{
1699
		// Choose the fastest and most robust way
1700
		if (function_exists('curl_init'))
1701 2
		{
1702
			$fetch_data = new CurlFetchWebdata([], $redirection_level);
1703
		}
1704
		elseif (empty(ini_get('allow_url_fopen')))
1705
		{
1706
			$fetch_data = new StreamFetchWebdata([], $redirection_level, $keep_alive);
1707 2
		}
1708
		else
1709
		{
1710
			$fetch_data = new FsockFetchWebdata([], $redirection_level, $keep_alive);
1711
		}
1712 2
1713
		// no errors and a 200 result, then we have a good dataset, well we at least have data ;)
1714
		$fetch_data->get_url_data($url, $post_data);
1715
		if ((int) $fetch_data->result('code') === 200 && !$fetch_data->result('error'))
1716
		{
1717
			return $fetch_data->result('body');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $fetch_data->result('body') also could return the type string[] which is incompatible with the documented return type false|string.
Loading history...
1718
		}
1719
1720
		return false;
1721
	}
1722
1723
	return $data;
1724
}
1725
1726
/**
1727
 * Checks if a package is installed or not
1728
 *
1729
 * - If installed, returns an array of themes, db changes and versions associated with
1730 2
 * the package id
1731
 *
1732 2
 * @param string $id of package to check
1733
 * @param string|null $install_id to check
1734
 *
1735
 * @return array
1736
 * @package Packages
1737
 */
1738
function isPackageInstalled($id, $install_id = null)
1739
{
1740 2
	$db = database();
1741 2
1742
	$result = [
1743
		'package_id' => null,
1744 2
		'install_state' => null,
1745
		'old_themes' => null,
1746 2
		'old_version' => null,
1747
		'db_changes' => []
1748
	];
1749
1750
	if (empty($id))
1751
	{
1752
		return $result;
1753
	}
1754
1755
	// See if it is installed?
1756
	$db->fetchQuery('
1757
		SELECT 
1758
			version, themes_installed, db_changes, package_id, install_state
1759
		FROM {db_prefix}log_packages
1760
		WHERE package_id = {string:current_package}
1761
			AND install_state != {int:not_installed}
1762
			' . (!empty($install_id) ? ' AND id_install = {int:install_id} ' : '') . '
1763 2
		ORDER BY time_installed DESC
1764
		LIMIT 1',
1765
		[
1766
			'not_installed' => 0,
1767
			'current_package' => $id,
1768 2
			'install_id' => $install_id,
1769
		]
1770
	)->fetch_callback(
1771
		function ($row) use (&$result) {
1772
			$result = [
1773
				'old_themes' => explode(',', $row['themes_installed']),
1774
				'old_version' => $row['version'],
1775
				'db_changes' => empty($row['db_changes']) ? [] : Util::unserialize($row['db_changes']),
1776
				'package_id' => $row['package_id'],
1777
				'install_state' => $row['install_state'],
1778
			];
1779
		}
1780
	);
1781
1782
	return $result;
1783
}
1784
1785
/**
1786
 * For uninstalling action, updates the log_packages install_state state to 0 (uninstalled)
1787
 *
1788
 * @param string $id package_id to update
1789 2
 * @param string $install_id install id of the package
1790
 * @package Packages
1791 2
 */
1792
function setPackageState($id, $install_id)
1793
{
1794
	$db = database();
1795
1796
	$db->query('', '
1797
		UPDATE {db_prefix}log_packages
1798
		SET 
1799
			install_state = {int:not_installed}, member_removed = {string:member_name}, 
1800
			id_member_removed = {int:current_member}, time_removed = {int:current_time}
1801
		WHERE package_id = {string:package_id}
1802
			AND id_install = {int:install_id}',
1803
		[
1804 2
			'current_member' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1805
			'not_installed' => 0,
1806
			'current_time' => time(),
1807
			'package_id' => $id,
1808
			'member_name' => User::$info->name,
0 ignored issues
show
Bug Best Practice introduced by
The property name does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1809 2
			'install_id' => $install_id,
1810
		]
1811
	);
1812 2
}
1813
1814
/**
1815
 * Checks if a package is installed, and if so returns its version level
1816
 *
1817
 * @param string $id
1818
 *
1819
 * @return string
1820
 * @package Packages
1821
 *
1822
 */
1823
function checkPackageDependency($id)
1824
{
1825
	$db = database();
1826 2
1827
	$version = '';
1828
	$db->fetchQuery('
1829 2
		SELECT 
1830
			version
1831
		FROM {db_prefix}log_packages
1832 2
		WHERE package_id = {string:current_package}
1833
			AND install_state != {int:not_installed}
1834
		ORDER BY time_installed DESC
1835
		LIMIT 1',
1836
		[
1837
			'not_installed' => 0,
1838
			'current_package' => $id,
1839
		]
1840
	)->fetch_callback(
1841
		function ($row) use (&$version) {
1842
			$version = $row['version'];
1843
		}
1844
	);
1845 2
1846 2
	return $version;
1847
}
1848 2
1849
/**
1850 2
 * Adds a record to the log packages table
1851
 *
1852
 * @param array $packageInfo
1853
 * @param string $failed_step_insert
1854
 * @param string $themes_installed
1855
 * @param string $db_changes
1856
 * @param bool $is_upgrade
1857
 * @param string $credits_tag
1858
 * @package Packages
1859 2
 */
1860
function addPackageLog($packageInfo, $failed_step_insert, $themes_installed, $db_changes, $is_upgrade, $credits_tag)
1861
{
1862
	$db = database();
1863
1864
	$db->insert('', '{db_prefix}log_packages',
1865 2
		[
1866
			'filename' => 'string', 'name' => 'string', 'package_id' => 'string', 'version' => 'string',
1867
			'id_member_installed' => 'int', 'member_installed' => 'string', 'time_installed' => 'int',
1868
			'install_state' => 'int', 'failed_steps' => 'string', 'themes_installed' => 'string',
1869
			'member_removed' => 'int', 'db_changes' => 'string', 'credits' => 'string',
1870
		],
1871
		[
1872
			$packageInfo['filename'], $packageInfo['name'], $packageInfo['id'], $packageInfo['version'],
1873
			User::$info->id, User::$info->name, time(),
0 ignored issues
show
Bug Best Practice introduced by
The property name does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1874
			$is_upgrade ? 2 : 1, $failed_step_insert, $themes_installed,
1875
			0, $db_changes, $credits_tag,
1876
		],
1877
		['id_install']
1878 2
	);
1879
}
1880
1881
/**
1882
 * Called from action_flush, used to flag all packages as uninstalled.
1883 2
 *
1884
 * @package Packages
1885 2
 */
1886 2
function setPackagesAsUninstalled()
1887
{
1888 2
	$db = database();
1889
1890 2
	// Set everything as uninstalled, just like that
1891
	$db->query('', '
1892
		UPDATE {db_prefix}log_packages
1893
		SET install_state = {int:not_installed}',
1894
		[
1895
			'not_installed' => 0,
1896
		]
1897
	);
1898
}
1899
1900 2
/**
1901
 * The ultimate writable test.
1902
 *
1903 2
 * @param $item
1904 2
 * @return bool
1905
 */
1906 2
function test_access($item)
1907
{
1908 2
	$fileFunc = FileFunctions::instance();
1909
1910
	$fp = $fileFunc->isDir($item) ? @opendir($item) : @fopen($item, 'rb');
1911
	if ($fileFunc->isWritable($item) && $fp)
0 ignored issues
show
introduced by
$fp is of type false|resource, thus it always evaluated to false.
Loading history...
1912
	{
1913
		if (!$fileFunc->isDir($item))
1914
		{
1915
			fclose($fp);
1916
		}
1917
		else
1918
		{
1919
			closedir($fp);
1920
		}
1921
1922
		return true;
1923
	}
1924
1925
	return false;
1926
}
1927
1928
/**
1929
 * Sets the base pathname as required by the FTP root (chrooted) directory
1930
 * e.g. /var/www/clients/client1/web1/webs/somefile.txt => /web1/webs/somefile.txt
1931
 *
1932
 * @param string $item
1933
 * @return string
1934
 */
1935
function setFtpName($item)
1936
{
1937
	$path_parts = pathinfo($item);
1938
	$path_parts['dirname'] = $path_parts['dirname'] === '.' ? '' : $path_parts['dirname'];
1939
	$path_parts['dirname'] = str_replace($_SESSION['ftp_connection']['root'], '', $path_parts['dirname']);
1940
1941
	return $path_parts['dirname'] . '/' . $path_parts['filename'];
1942
}
1943