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 0
Metric Value
cc 77
eloc 179
nc 3292933
nop 4
dl 0
loc 393
ccs 84
cts 178
cp 0.4719
crap 950.2326
rs 0
c 0
b 0
f 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 Beta 1
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 website
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 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
	// the 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 an 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 a 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
	return (new PackageChmod())->createChmodControl($chmodFiles, $chmodOptions, $restore_write_status);
289
}
290
291
/**
292
 * Get a listing of files that will need to be set back to the original state
293
 *
294
 * @param string $dummy1
295
 * @param string $dummy2
296 4
 * @param string $dummy3
297
 * @param bool $do_change
298
 *
299 4
 * @return array
300
 */
301
function list_restoreFiles($dummy1, $dummy2, $dummy3, $do_change)
0 ignored issues
show
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

301
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 $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

301
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 $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

301
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...
302
{
303
	global $txt, $package_ftp;
304 4
305
	$restore_files = [];
306
	$fileFunc = FileFunctions::instance();
307 4
308 4
	foreach ($_SESSION['ftp_connection']['original_perms'] as $file => $perms)
309 4
	{
310
		// Check the file still exists, and the permissions were indeed different from now.
311
		$file_permissions = $fileFunc->filePerms($file);
312 4
		if (!$fileFunc->fileExists($file) || $file_permissions === $perms)
313
		{
314
			unset($_SESSION['ftp_connection']['original_perms'][$file]);
315
			continue;
316
		}
317 4
318
		// Are we wanting to change the permission?
319
		if ($do_change && isset($_POST['restore_files']) && in_array($file, $_POST['restore_files']))
320
		{
321
			// Use FTP if we have it.
322
			if (!empty($package_ftp))
323
			{
324
				$ftp_file = setFtpName($file);
325
				$package_ftp->chmod($ftp_file, $perms);
326
			}
327
			else
328
			{
329
				$fileFunc->elk_chmod($file, $perms);
330
			}
331
332 4
			$new_permissions = $fileFunc->filePerms($file);
333
			$result = $new_permissions === $perms ? 'success' : 'failure';
334
			unset($_SESSION['ftp_connection']['original_perms'][$file]);
335 4
		}
336
		elseif ($do_change)
337
		{
338
			$new_permissions = '';
339
			$result = 'skipped';
340
			unset($_SESSION['ftp_connection']['original_perms'][$file]);
341
		}
342
343
		// Record the results!
344
		$restore_files[] = [
345
			'path' => $file,
346
			'old_perms_raw' => $perms,
347
			'old_perms' => substr(sprintf('%o', $perms), -4),
348
			'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

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

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

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

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

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

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

1570
				if (preg_match('~^(\.{1,2}|CVS|backup.*|help|images|.*\~)$~', /** @scrutinizer ignore-type */ $entry) != 0)
Loading history...
1571
				{
1572
					continue;
1573 32
				}
1574
1575 32
				$files[$use_relative_paths ? str_replace(realpath(BOARDDIR), '', $entry) : $entry] = $entry;
1576
			}
1577
		}
1578
1579
		// Make sure we have a backup directory and it's writable
1580
		if (!$fileFunc->fileExists(BOARDDIR . '/packages/backups'))
1581 1
		{
1582
			$fileFunc->createDirectory(BOARDDIR . '/packages/backups');
1583
		}
1584
1585 2
		if (!$fileFunc->isWritable(BOARDDIR . '/packages/backups'))
1586
		{
1587
			$packageChmod = new PackageChmod();
1588
			$packageChmod->pkgChmod(BOARDDIR . '/packages/backups');
1589
		}
1590
1591
		// Name the output file, yyyy-mm-dd_before_package_name.tar.gz
1592
		$output_file = BOARDDIR . '/packages/backups/' . Util::strftime('%Y-%m-%d_') . preg_replace('~[$\\\\/:<>|?*"\']~', '', $id);
1593
		$output_ext = '.tar';
1594
1595
		if ($fileFunc->fileExists($output_file . $output_ext . '.gz'))
1596
		{
1597
			$i = 2;
1598
			while ($fileFunc->fileExists($output_file . '_' . $i . $output_ext . '.gz'))
1599
			{
1600
				$i++;
1601
			}
1602 32
			$output_file .= '_' . $i . $output_ext;
1603
		}
1604 32
		else
1605 32
		{
1606
			$output_file .= $output_ext;
1607
		}
1608 32
1609 32
		// Buy some more time, so we have enough to create this archive
1610
		detectServer()->setTimeLimit(300);
1611
1612 32
		$phar = new PharData($output_file);
1613 32
		$phar->buildFromIterator($files);
1614 32
		$phar->compress(Phar::GZ);
1615 32
1616 32
		/*
1617 32
		 * Destroying the local var tells PharData to close its internal
1618 32
		 * file pointer, enabling us to delete the uncompressed tarball.
1619 32
		 */
1620
		unset($phar);
1621
		unlink($output_file);
1622
	}
1623
	catch (Exception $e)
1624 32
	{
1625
		\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...
1626 32
1627
		return false;
1628
	}
1629
1630 32
	return true;
1631
}
1632
1633
/**
1634
 * Get the contents of a URL, irrespective of allow_url_fopen.
1635
 *
1636 32
 * - Reads the contents of http or ftp addresses and returns the page in a string.
1637
 * - Will accept up to (3) redirections (redirection_level in the function call is private).
1638
 * - If post_data is supplied, the value and length are posted to the given url as form data.
1639 32
 * - URL must be supplied in lowercase
1640
 *
1641
 * @param string $url
1642
 * @param string $post_data = ''
1643 32
 * @param bool $keep_alive = false
1644
 * @param int $redirection_level = 3
1645
 * @return false|string
1646
 * @package Packages
1647 32
 */
1648
function fetch_web_data($url, $post_data = '', $keep_alive = false, $redirection_level = 3)
1649
{
1650
	global $webmaster_email;
1651
1652
	preg_match('~^(http|ftp)(s)?://([^/:]+)(:(\d+))?(.+)$~', $url, $match);
1653
	$data = '';
1654 32
1655
	// An FTP url. We should try connecting and RETRieving it...
1656
	if (empty($match[1]))
1657
	{
1658
		return false;
1659
	}
1660
1661
	if ($match[1] === 'ftp')
1662
	{
1663
		// Establish a connection and attempt to enable passive mode.
1664
		$ftp = new FtpConnection(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? 21 : $match[5], 'anonymous', $webmaster_email);
1665
		if ($ftp->error !== false || !$ftp->passive())
1666
		{
1667
			return false;
1668
		}
1669
1670
		// I want that one *points*!
1671
		fwrite($ftp->connection, 'RETR ' . $match[6] . "\r\n");
1672 2
1673
		// Since passive mode worked (or we would have returned already!) open the connection.
1674 2
		$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...
1675
		if (!$fp)
0 ignored issues
show
introduced by
$fp is of type false|resource, thus it always evaluated to false.
Loading history...
1676
		{
1677
			return false;
1678
		}
1679
1680 2
		// The server should now say something in acknowledgement.
1681
		$ftp->check_response(150);
1682
1683
		while (!feof($fp))
1684
		{
1685
			$data .= fread($fp, 4096);
1686
		}
1687
		fclose($fp);
1688 2
1689 2
		// All done, right?  Good.
1690 2
		$ftp->check_response(226);
1691 2
		$ftp->close();
1692 2
	}
1693
	// More likely a standard HTTP URL, first try to use cURL if available
1694
	elseif ($match[1] === 'http')
1695
	{
1696 2
		// Choose the fastest and most robust way
1697
		if (function_exists('curl_init'))
1698 2
		{
1699
			$fetch_data = new CurlFetchWebdata([], $redirection_level);
1700
		}
1701 2
		elseif (empty(ini_get('allow_url_fopen')))
1702
		{
1703
			$fetch_data = new StreamFetchWebdata([], $redirection_level, $keep_alive);
1704
		}
1705
		else
1706
		{
1707 2
			$fetch_data = new FsockFetchWebdata([], $redirection_level, $keep_alive);
1708
		}
1709
1710
		// no errors and a 200 result, then we have a good dataset, well, we at least have data ;)
1711
		$fetch_data->get_url_data($url, $post_data);
1712 2
		if ((int) $fetch_data->result('code') === 200 && !$fetch_data->result('error'))
1713
		{
1714
			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...
1715
		}
1716
1717
		return false;
1718
	}
1719
1720
	return $data;
1721
}
1722
1723
/**
1724
 * Checks if a package is installed or not
1725
 *
1726
 * - If installed, returns an array of themes, db changes, and versions associated with
1727
 * the package id
1728
 *
1729
 * @param string $id of package to check
1730 2
 * @param string|null $install_id to check
1731
 *
1732 2
 * @return array
1733
 * @package Packages
1734
 */
1735
function isPackageInstalled($id, $install_id = null)
1736
{
1737
	$db = database();
1738
1739
	$result = [
1740 2
		'package_id' => null,
1741 2
		'install_state' => null,
1742
		'old_themes' => null,
1743
		'old_version' => null,
1744 2
		'db_changes' => []
1745
	];
1746 2
1747
	if (empty($id))
1748
	{
1749
		return $result;
1750
	}
1751
1752
	// See if it is installed?
1753
	$db->fetchQuery('
1754
		SELECT 
1755
			version, themes_installed, db_changes, package_id, install_state
1756
		FROM {db_prefix}log_packages
1757
		WHERE package_id = {string:current_package}
1758
			AND install_state != {int:not_installed}
1759
			' . (!empty($install_id) ? ' AND id_install = {int:install_id} ' : '') . '
1760
		ORDER BY time_installed DESC
1761
		LIMIT 1',
1762
		[
1763 2
			'not_installed' => 0,
1764
			'current_package' => $id,
1765
			'install_id' => $install_id,
1766
		]
1767
	)->fetch_callback(
1768 2
		function ($row) use (&$result) {
1769
			$result = [
1770
				'old_themes' => explode(',', $row['themes_installed']),
1771
				'old_version' => $row['version'],
1772
				'db_changes' => empty($row['db_changes']) ? [] : Util::unserialize($row['db_changes']),
1773
				'package_id' => $row['package_id'],
1774
				'install_state' => $row['install_state'],
1775
			];
1776
		}
1777
	);
1778
1779
	return $result;
1780
}
1781
1782
/**
1783
 * For uninstalling action, updates the log_packages install_state state to 0 (uninstalled)
1784
 *
1785
 * @param string $id package_id to update
1786
 * @param string $install_id install id of the package
1787
 * @package Packages
1788
 */
1789 2
function setPackageState($id, $install_id)
1790
{
1791 2
	$db = database();
1792
1793
	$db->query('', '
1794
		UPDATE {db_prefix}log_packages
1795
		SET 
1796
			install_state = {int:not_installed}, member_removed = {string:member_name}, 
1797
			id_member_removed = {int:current_member}, time_removed = {int:current_time}
1798
		WHERE package_id = {string:package_id}
1799
			AND id_install = {int:install_id}',
1800
		[
1801
			'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...
1802
			'not_installed' => 0,
1803
			'current_time' => time(),
1804 2
			'package_id' => $id,
1805
			'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...
1806
			'install_id' => $install_id,
1807
		]
1808
	);
1809 2
}
1810
1811
/**
1812 2
 * Checks if a package is installed, and if so, returns its version level
1813
 *
1814
 * @param string $id
1815
 *
1816
 * @return string
1817
 * @package Packages
1818
 *
1819
 */
1820
function checkPackageDependency($id)
1821
{
1822
	$db = database();
1823
1824
	$version = '';
1825
	$db->fetchQuery('
1826 2
		SELECT 
1827
			version
1828
		FROM {db_prefix}log_packages
1829 2
		WHERE package_id = {string:current_package}
1830
			AND install_state != {int:not_installed}
1831
		ORDER BY time_installed DESC
1832 2
		LIMIT 1',
1833
		[
1834
			'not_installed' => 0,
1835
			'current_package' => $id,
1836
		]
1837
	)->fetch_callback(
1838
		function ($row) use (&$version) {
1839
			$version = $row['version'];
1840
		}
1841
	);
1842
1843
	return $version;
1844
}
1845 2
1846 2
/**
1847
 * Adds a record to the log packages table
1848 2
 *
1849
 * @param array $packageInfo
1850 2
 * @param string $failed_step_insert
1851
 * @param string $themes_installed
1852
 * @param string $db_changes
1853
 * @param bool $is_upgrade
1854
 * @param string $credits_tag
1855
 * @package Packages
1856
 */
1857
function addPackageLog($packageInfo, $failed_step_insert, $themes_installed, $db_changes, $is_upgrade, $credits_tag)
1858
{
1859 2
	$db = database();
1860
1861
	$db->insert('', '{db_prefix}log_packages',
1862
		[
1863
			'filename' => 'string', 'name' => 'string', 'package_id' => 'string', 'version' => 'string',
1864
			'id_member_installed' => 'int', 'member_installed' => 'string', 'time_installed' => 'int',
1865 2
			'install_state' => 'int', 'failed_steps' => 'string', 'themes_installed' => 'string',
1866
			'member_removed' => 'int', 'db_changes' => 'string', 'credits' => 'string',
1867
		],
1868
		[
1869
			$packageInfo['filename'], $packageInfo['name'], $packageInfo['id'], $packageInfo['version'],
1870
			User::$info->id, User::$info->name, time(),
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...
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...
1871
			$is_upgrade ? 2 : 1, $failed_step_insert, $themes_installed,
1872
			0, $db_changes, $credits_tag,
1873
		],
1874
		['id_install']
1875
	);
1876
}
1877
1878 2
/**
1879
 * Called from action_flush, used to flag all packages as uninstalled.
1880
 *
1881
 * @package Packages
1882
 */
1883 2
function setPackagesAsUninstalled()
1884
{
1885 2
	$db = database();
1886 2
1887
	// Set everything as uninstalled, just like that
1888 2
	$db->query('', '
1889
		UPDATE {db_prefix}log_packages
1890 2
		SET install_state = {int:not_installed}',
1891
		[
1892
			'not_installed' => 0,
1893
		]
1894
	);
1895
}
1896
1897
/**
1898
 * The ultimate writable test.
1899
 *
1900 2
 * @param $item
1901
 * @return bool
1902
 */
1903 2
function test_access($item)
1904 2
{
1905
	$fileFunc = FileFunctions::instance();
1906 2
1907
	$fp = $fileFunc->isDir($item) ? @opendir($item) : @fopen($item, 'rb');
1908 2
	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...
1909
	{
1910
		if (!$fileFunc->isDir($item))
1911
		{
1912
			fclose($fp);
1913
		}
1914
		else
1915
		{
1916
			closedir($fp);
1917
		}
1918
1919
		return true;
1920
	}
1921
1922
	return false;
1923
}
1924
1925
/**
1926
 * Sets the base pathname as required by the FTP root (chrooted) directory
1927
 * e.g., /var/www/clients/client1/web1/webs/somefile.txt => /web1/webs/somefile.txt
1928
 *
1929
 * @param string $item
1930
 * @return string
1931
 */
1932
function setFtpName($item)
1933
{
1934
	$path_parts = pathinfo($item);
1935
	$path_parts['dirname'] = $path_parts['dirname'] === '.' ? '' : $path_parts['dirname'];
1936
	$path_parts['dirname'] = str_replace($_SESSION['ftp_connection']['root'], '', $path_parts['dirname']);
1937
1938
	return $path_parts['dirname'] . '/' . $path_parts['filename'];
1939
}
1940