Failed Conditions
Branch release-2.1 (4e22cf)
by Rick
06:39
created

Subs-Package.php ➔ package_put_contents()   C

Complexity

Conditions 17
Paths 90

Size

Total Lines 53
Code Lines 31

Duplication

Lines 16
Ratio 30.19 %

Importance

Changes 0
Metric Value
cc 17
eloc 31
nc 90
nop 3
dl 16
loc 53
rs 6.2566
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 file's central purpose of existence is that of making the package
5
 * manager work nicely.  It contains functions for handling tar.gz and zip
6
 * files, as well as a simple xml parser to handle the xml package stuff.
7
 * Not to mention a few functions to make file handling easier.
8
 *
9
 * Simple Machines Forum (SMF)
10
 *
11
 * @package SMF
12
 * @author Simple Machines http://www.simplemachines.org
13
 * @copyright 2017 Simple Machines and individual contributors
14
 * @license http://www.simplemachines.org/about/smf/license.php BSD
15
 *
16
 * @version 2.1 Beta 4
17
 */
18
19
if (!defined('SMF'))
20
	die('No direct access...');
21
22
/**
23
 * Reads a .tar.gz file, filename, in and extracts file(s) from it.
24
 * essentially just a shortcut for read_tgz_data().
25
 *
26
 * @param string $gzfilename The path to the tar.gz file
27
 * @param string $destination The path to the desitnation directory
28
 * @param bool $single_file If true returns the contents of the file specified by destination if it exists
29
 * @param bool $overwrite Whether to overwrite existing files
30
 * @param null|array $files_to_extract Specific files to extract
31
 * @return array|false An array of information about extracted files or false on failure
32
 */
33
function read_tgz_file($gzfilename, $destination, $single_file = false, $overwrite = false, $files_to_extract = null)
34
{
35
	return read_tgz_data($gzfilename, $destination, $single_file, $overwrite, $files_to_extract);
36
}
37
38
/**
39
 * Extracts a file or files from the .tar.gz contained in data.
40
 *
41
 * detects if the file is really a .zip file, and if so returns the result of read_zip_data
42
 *
43
 * if destination is null
44
 *	- returns a list of files in the archive.
45
 *
46
 * if single_file is true
47
 * - returns the contents of the file specified by destination, if it exists, or false.
48
 * - destination can start with * and / to signify that the file may come from any directory.
49
 * - destination should not begin with a / if single_file is true.
50
 *
51
 * overwrites existing files with newer modification times if and only if overwrite is true.
52
 * creates the destination directory if it doesn't exist, and is is specified.
53
 * requires zlib support be built into PHP.
54
 * returns an array of the files extracted.
55
 * if files_to_extract is not equal to null only extracts file within this array.
56
 *
57
 * @param string $gzfilename The name of the file
58
 * @param string $destination The destination
59
 * @param bool $single_file Whether to only extract a single file
60
 * @param bool $overwrite Whether to overwrite existing data
61
 * @param null|array $files_to_extract If set, only extracts the specified files
62
 * @return array|false An array of information about the extracted files or false on failure
63
 */
64
function read_tgz_data($gzfilename, $destination, $single_file = false, $overwrite = false, $files_to_extract = null)
65
{
66
	// Make sure we have this loaded.
67
	loadLanguage('Packages');
68
69
	// This function sorta needs gzinflate!
70
	if (!function_exists('gzinflate'))
71
		fatal_lang_error('package_no_zlib', 'critical');
72
73
	if (substr($gzfilename, 0, 7) == 'http://' || substr($gzfilename, 0, 8) == 'https://')
74
	{
75
		$data = fetch_web_data($gzfilename);
76
77
		if ($data === false)
78
			return false;
79
	}
80
	else
81
	{
82
		$data = @file_get_contents($gzfilename);
83
84
		if ($data === false)
85
			return false;
86
	}
87
88
	umask(0);
89 View Code Duplication
	if (!$single_file && $destination !== null && !file_exists($destination))
90
		mktree($destination, 0777);
91
92
	// No signature?
93
	if (strlen($data) < 2)
94
		return false;
95
96
	$id = unpack('H2a/H2b', substr($data, 0, 2));
97
	if (strtolower($id['a'] . $id['b']) != '1f8b')
98
	{
99
		// Okay, this ain't no tar.gz, but maybe it's a zip file.
100
		if (substr($data, 0, 2) == 'PK')
101
			return read_zip_file($gzfilename, $destination, $single_file, $overwrite, $files_to_extract);
102
		else
103
			return false;
104
	}
105
106
	$flags = unpack('Ct/Cf', substr($data, 2, 2));
107
108
	// Not deflate!
109
	if ($flags['t'] != 8)
110
		return false;
111
	$flags = $flags['f'];
112
113
	$offset = 10;
114
	$octdec = array('mode', 'uid', 'gid', 'size', 'mtime', 'checksum', 'type');
115
116
	// "Read" the filename and comment.
117
	// @todo Might be mussed.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
118
	if ($flags & 12)
119
	{
120
		while ($flags & 8 && $data{$offset++} != "\0")
121
			continue;
122
		while ($flags & 4 && $data{$offset++} != "\0")
123
			continue;
124
	}
125
126
	$crc = unpack('Vcrc32/Visize', substr($data, strlen($data) - 8, 8));
127
	$data = @gzinflate(substr($data, $offset, strlen($data) - 8 - $offset));
128
129
	// smf_crc32 and crc32 may not return the same results, so we accept either.
130
	if ($crc['crc32'] != smf_crc32($data) && $crc['crc32'] != crc32($data))
131
		return false;
132
133
	$blocks = strlen($data) / 512 - 1;
134
	$offset = 0;
135
136
	$return = array();
137
138
	while ($offset < $blocks)
139
	{
140
		$header = substr($data, $offset << 9, 512);
141
		$current = unpack('a100filename/a8mode/a8uid/a8gid/a12size/a12mtime/a8checksum/a1type/a100linkname/a6magic/a2version/a32uname/a32gname/a8devmajor/a8devminor/a155path', $header);
142
143
		// Blank record?  This is probably at the end of the file.
144
		if (empty($current['filename']))
145
		{
146
			$offset += 512;
147
			continue;
148
		}
149
150
		foreach ($current as $k => $v)
151
		{
152
			if (in_array($k, $octdec))
153
				$current[$k] = octdec(trim($v));
154
			else
155
				$current[$k] = trim($v);
156
		}
157
158
		if ($current['type'] == 5 && substr($current['filename'], -1) != '/')
159
			$current['filename'] .= '/';
160
161
		$checksum = 256;
162 View Code Duplication
		for ($i = 0; $i < 148; $i++)
163
			$checksum += ord($header{$i});
164 View Code Duplication
		for ($i = 156; $i < 512; $i++)
165
			$checksum += ord($header{$i});
166
167
		if ($current['checksum'] != $checksum)
168
			break;
169
170
		$size = ceil($current['size'] / 512);
171
		$current['data'] = substr($data, ++$offset << 9, $current['size']);
172
		$offset += $size;
173
174
		// Not a directory and doesn't exist already...
175
		if (substr($current['filename'], -1, 1) != '/' && !file_exists($destination . '/' . $current['filename']))
176
			$write_this = true;
177
		// File exists... check if it is newer.
178
		elseif (substr($current['filename'], -1, 1) != '/')
179
			$write_this = $overwrite || filemtime($destination . '/' . $current['filename']) < $current['mtime'];
180
		// Folder... create.
181 View Code Duplication
		elseif ($destination !== null && !$single_file)
182
		{
183
			// Protect from accidental parent directory writing...
184
			$current['filename'] = strtr($current['filename'], array('../' => '', '/..' => ''));
185
186
			if (!file_exists($destination . '/' . $current['filename']))
187
				mktree($destination . '/' . $current['filename'], 0777);
188
			$write_this = false;
189
		}
190
		else
191
			$write_this = false;
192
193
		if ($write_this && $destination !== null)
194
		{
195
			if (strpos($current['filename'], '/') !== false && !$single_file)
196
				mktree($destination . '/' . dirname($current['filename']), 0777);
197
198
			// Is this the file we're looking for?
199 View Code Duplication
			if ($single_file && ($destination == $current['filename'] || $destination == '*/' . basename($current['filename'])))
200
				return $current['data'];
201
			// If we're looking for another file, keep going.
202
			elseif ($single_file)
203
				continue;
204
			// Looking for restricted files?
205
			elseif ($files_to_extract !== null && !in_array($current['filename'], $files_to_extract))
206
				continue;
207
208
			package_put_contents($destination . '/' . $current['filename'], $current['data']);
209
		}
210
211 View Code Duplication
		if (substr($current['filename'], -1, 1) != '/')
212
			$return[] = array(
213
				'filename' => $current['filename'],
214
				'md5' => md5($current['data']),
215
				'preview' => substr($current['data'], 0, 100),
216
				'size' => $current['size'],
217
				'skipped' => false
218
			);
219
	}
220
221
	if ($destination !== null && !$single_file)
222
		package_flush_cache();
223
224
	if ($single_file)
225
		return false;
226
	else
227
		return $return;
228
}
229
230
/**
231
 * Extract zip data. A functional copy of {@list read_zip_data()}.
232
 *
233
 * @param string $file Input filename
234
 * @param string $destination Null to display a listing of files in the archive, the destination for the files in the archive or the name of a single file to display (if $single_file is true)
235
 * @param boolean $single_file If true, returns the contents of the file specified by destination or false if the file can't be found (default value is false).
236
 * @param boolean $overwrite If true, will overwrite files with newer modication times. Default is false.
237
 * @param array $files_to_extract Specific files to extract
238
 * @uses {@link PharData}
0 ignored issues
show
Documentation introduced by
Should the type for parameter $files_to_extract not be array|null? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
239
 * @return mixed If destination is null, return a short array of a few file details optionally delimited by $files_to_extract. If $single_file is true, return contents of a file as a string; false otherwise
240
 */
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use string|null|false|array.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
241
242
function read_zip_file($file, $destination, $single_file = false, $overwrite = false, $files_to_extract = null)
243
{
244
	try
245
	{
246
		// This may not always be defined...
247
		$return = array();
248
249
		$archive = new PharData($file, RecursiveIteratorIterator::SELF_FIRST, null, Phar::ZIP);
250
		$iterator = new RecursiveIteratorIterator($archive, RecursiveIteratorIterator::SELF_FIRST);
251
252
		// go though each file in the archive
253
		foreach ($iterator as $file_info)
254
			{
255
				$i = $iterator->getSubPathname();
256
				// If this is a file, and it doesn't exist.... happy days!
257
				if (substr($i, -1) != '/' && !file_exists($destination . '/' . $i))
258
					$write_this = true;
259
				// If the file exists, we may not want to overwrite it.
260
				elseif (substr($i, -1) != '/')
261
					$write_this = $overwrite;
262
				else
263
					$write_this = false;
264
265
				// Get the actual compressed data.
266
				if (!$file_info->isDir())
267
					$file_data = file_get_contents($file_info);
268
				elseif ($destination !== null && !$single_file)
269
				{
270
					// Folder... create.
271
					if (!file_exists($destination . '/' . $i))
272
						mktree($destination . '/' . $i, 0777);
273
					$file_data = null;
274
				}
275
				else
276
					$file_data = null;
277
278
				// Okay!  We can write this file, looks good from here...
279
				if ($write_this && $destination !== null)
280
				{
281
					if (!$single_file && !is_dir($destination . '/' . dirname($i)))
282
						mktree($destination . '/' . dirname($i), 0777);
283
284
					// If we're looking for a specific file, and this is it... ka-bam, baby.
285
					if ($single_file && ($destination == $i || $destination == '*/' . basename($i)))
286
						return $file_data;
287
					// Oh?  Another file.  Fine.  You don't like this file, do you?  I know how it is.  Yeah... just go away.  No, don't apologize.  I know this file's just not *good enough* for you.
288
					elseif ($single_file)
289
						continue;
290
					// Don't really want this?
291
					elseif ($files_to_extract !== null && !in_array($i, $files_to_extract))
292
						continue;
293
294
					package_put_contents($destination . '/' . $i, $file_data);
295
				}
296
297
				if (substr($i, -1, 1) != '/')
298
					$return[] = array(
299
						'filename' => $i,
300
						'md5' => md5($file_data),
301
						'preview' => substr($file_data, 0, 100),
302
						'size' => strlen($file_data),
303
						'skipped' => false
304
					);
305
			}
306
307
		if ($destination !== null && !$single_file)
308
			package_flush_cache();
309
310
		if ($single_file)
311
			return false;
312
		else
313
			return $return;
314
	}
315
	catch (Exception $e)
316
	{
317
		return false;
318
	}
319
}
320
321
/**
322
 * Extract zip data. .
323
 *
324
 * If single_file is true, destination can start with * and / to signify that the file may come from any directory.
325
 * Destination should not begin with a / if single_file is true.
326
 *
327
 * @param string $data ZIP data
328
 * @param string $destination Null to display a listing of files in the archive, the destination for the files in the archive or the name of a single file to display (if $single_file is true)
329
 * @param boolean $single_file If true, returns the contents of the file specified by destination or false if the file can't be found (default value is false).
330
 * @param boolean $overwrite If true, will overwrite files with newer modication times. Default is false.
331
 * @param array $files_to_extract
0 ignored issues
show
Documentation introduced by
Should the type for parameter $files_to_extract not be array|null? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
332
 * @return mixed If destination is null, return a short array of a few file details optionally delimited by $files_to_extract. If $single_file is true, return contents of a file as a string; false otherwise
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use false|string|array.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
333
 */
334
function read_zip_data($data, $destination, $single_file = false, $overwrite = false, $files_to_extract = null)
335
{
336
	umask(0);
337 View Code Duplication
	if ($destination !== null && !file_exists($destination) && !$single_file)
338
		mktree($destination, 0777);
339
340
	// Look for the end of directory signature 0x06054b50
341
	$data_ecr = explode("\x50\x4b\x05\x06", $data);
342
	if (!isset($data_ecr[1]))
343
		return false;
344
345
	$return = array();
346
347
	// Get all the basic zip file info since we are here
348
	$zip_info = unpack('vdisks/vrecords/vfiles/Vsize/Voffset/vcomment_length/', $data_ecr[1]);
349
350
	// Cut file at the central directory file header signature -- 0x02014b50, use unpack if you want any of the data, we don't
351
	$file_sections = explode("\x50\x4b\x01\x02", $data);
352
353
	// Cut the result on each local file header -- 0x04034b50 so we have each file in the archive as an element.
354
	$file_sections = explode("\x50\x4b\x03\x04", $file_sections[0]);
355
	array_shift($file_sections);
356
357
	// sections and count from the signature must match or the zip file is bad
358
	if (count($file_sections) != $zip_info['files'])
359
		return false;
360
361
	// go though each file in the archive
362
	foreach ($file_sections as $data)
363
	{
364
		// Get all the important file information.
365
		$file_info = unpack("vversion/vgeneral_purpose/vcompress_method/vfile_time/vfile_date/Vcrc/Vcompressed_size/Vsize/vfilename_length/vextrafield_length", $data);
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal vversion/vgeneral_purpos...ngth/vextrafield_length does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
366
		$file_info['filename'] = substr($data, 26, $file_info['filename_length']);
367
		$file_info['dir'] = $destination . '/' . dirname($file_info['filename']);
368
369
		// If bit 3 (0x08) of the general-purpose flag is set, then the CRC and file size were not available when the header was written
370
		// In this case the CRC and size are instead appended in a 12-byte structure immediately after the compressed data
371
		if ($file_info['general_purpose'] & 0x0008)
372
		{
373
			$unzipped2 = unpack("Vcrc/Vcompressed_size/Vsize", substr($$data, -12));
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal Vcrc/Vcompressed_size/Vsize does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
374
			$file_info['crc'] = $unzipped2['crc'];
375
			$file_info['compressed_size'] = $unzipped2['compressed_size'];
376
			$file_info['size'] = $unzipped2['size'];
377
			unset($unzipped2);
378
		}
379
380
		// If this is a file, and it doesn't exist.... happy days!
381
		if (substr($file_info['filename'], -1) != '/' && !file_exists($destination . '/' . $file_info['filename']))
382
			$write_this = true;
383
		// If the file exists, we may not want to overwrite it.
384
		elseif (substr($file_info['filename'], -1) != '/')
385
			$write_this = $overwrite;
386
		// This is a directory, so we're gonna want to create it. (probably...)
387 View Code Duplication
		elseif ($destination !== null && !$single_file)
388
		{
389
			// Just a little accident prevention, don't mind me.
390
			$file_info['filename'] = strtr($file_info['filename'], array('../' => '', '/..' => ''));
391
392
			if (!file_exists($destination . '/' . $file_info['filename']))
393
				mktree($destination . '/' . $file_info['filename'], 0777);
394
			$write_this = false;
395
		}
396
		else
397
			$write_this = false;
398
399
		// Get the actual compressed data.
400
		$file_info['data'] = substr($data, 26 + $file_info['filename_length'] + $file_info['extrafield_length']);
401
402
		// Only inflate it if we need to ;)
403
		if (!empty($file_info['compress_method']) || ($file_info['compressed_size'] != $file_info['size']))
404
			$file_info['data'] = gzinflate($file_info['data']);
405
406
		// Okay!  We can write this file, looks good from here...
407
		if ($write_this && $destination !== null)
408
		{
409
			if ((strpos($file_info['filename'], '/') !== false && !$single_file) || (!$single_file && !is_dir($file_info['dir'])))
410
				mktree($file_info['dir'], 0777);
411
412
			// If we're looking for a specific file, and this is it... ka-bam, baby.
413 View Code Duplication
			if ($single_file && ($destination == $file_info['filename'] || $destination == '*/' . basename($file_info['filename'])))
414
				return $file_info['data'];
415
			// Oh?  Another file.  Fine.  You don't like this file, do you?  I know how it is.  Yeah... just go away.  No, don't apologize.  I know this file's just not *good enough* for you.
416
			elseif ($single_file)
417
				continue;
418
			// Don't really want this?
419
			elseif ($files_to_extract !== null && !in_array($file_info['filename'], $files_to_extract))
420
				continue;
421
422
			package_put_contents($destination . '/' . $file_info['filename'], $file_info['data']);
423
		}
424
425 View Code Duplication
		if (substr($file_info['filename'], -1, 1) != '/')
426
			$return[] = array(
427
				'filename' => $file_info['filename'],
428
				'md5' => md5($file_info['data']),
429
				'preview' => substr($file_info['data'], 0, 100),
430
				'size' => $file_info['size'],
431
				'skipped' => false
432
			);
433
	}
434
435
	if ($destination !== null && !$single_file)
436
		package_flush_cache();
437
438
	if ($single_file)
439
		return false;
440
	else
441
		return $return;
442
}
443
444
/**
445
 * Checks the existence of a remote file since file_exists() does not do remote.
446
 * will return false if the file is "moved permanently" or similar.
447
 * @param string $url The URL to parse
448
 * @return bool Whether the specified URL exists
449
 */
450
function url_exists($url)
451
{
452
	$a_url = parse_url($url);
453
454
	if (!isset($a_url['scheme']))
455
		return false;
456
457
	// Attempt to connect...
458
	$temp = '';
459
	$fid = fsockopen($a_url['host'], !isset($a_url['port']) ? 80 : $a_url['port'], $temp, $temp, 8);
460
	if (!$fid)
461
		return false;
462
463
	fputs($fid, 'HEAD ' . $a_url['path'] . ' HTTP/1.0' . "\r\n" . 'Host: ' . $a_url['host'] . "\r\n\r\n");
464
	$head = fread($fid, 1024);
465
	fclose($fid);
466
467
	return preg_match('~^HTTP/.+\s+(20[01]|30[127])~i', $head) == 1;
468
}
469
470
/**
471
 * Loads and returns an array of installed packages.
472
 * - returns the array of data.
473
 * - default sort order is package_installed time
474
 *
475
 * @return array An array of info about installed packages
476
 */
477
function loadInstalledPackages()
478
{
479
	global $smcFunc;
480
481
	// Load the packages from the database - note this is ordered by install time to ensure latest package uninstalled first.
482
	$request = $smcFunc['db_query']('', '
483
		SELECT id_install, package_id, filename, name, version, time_installed
484
		FROM {db_prefix}log_packages
485
		WHERE install_state != {int:not_installed}
486
		ORDER BY time_installed DESC',
487
		array(
488
			'not_installed' => 0,
489
		)
490
	);
491
	$installed = array();
492
	$found = array();
493
	while ($row = $smcFunc['db_fetch_assoc']($request))
494
	{
495
		// Already found this? If so don't add it twice!
496
		if (in_array($row['package_id'], $found))
497
			continue;
498
499
		$found[] = $row['package_id'];
500
501
		$row = htmlspecialchars__recursive($row);
502
503
		$installed[] = array(
504
			'id' => $row['id_install'],
505
			'name' => $smcFunc['htmlspecialchars']($row['name']),
506
			'filename' => $row['filename'],
507
			'package_id' => $row['package_id'],
508
			'version' => $smcFunc['htmlspecialchars']($row['version']),
509
			'time_installed' => !empty($row['time_installed']) ? $row['time_installed'] : 0,
510
		);
511
	}
512
	$smcFunc['db_free_result']($request);
513
514
	return $installed;
515
}
516
517
/**
518
 * Loads a package's information and returns a representative array.
519
 * - expects the file to be a package in Packages/.
520
 * - returns a error string if the package-info is invalid.
521
 * - otherwise returns a basic array of id, version, filename, and similar information.
522
 * - an xmlArray is available in 'xml'.
523
 *
524
 * @param string $gzfilename The path to the file
525
 * @return array|string An array of info about the file or a string indicating an error
526
 */
527
function getPackageInfo($gzfilename)
528
{
529
	global $sourcedir, $packagesdir;
530
531
	// Extract package-info.xml from downloaded file. (*/ is used because it could be in any directory.)
532
	if (strpos($gzfilename, 'http://') !== false || strpos($gzfilename, 'https://') !== false)
533
		$packageInfo = read_tgz_data($gzfilename, 'package-info.xml', true);
534
	else
535
	{
536
		if (!file_exists($packagesdir . '/' . $gzfilename))
537
			return 'package_get_error_not_found';
538
539
		if (is_file($packagesdir . '/' . $gzfilename))
540
			$packageInfo = read_tgz_file($packagesdir . '/' . $gzfilename, '*/package-info.xml', true);
541
		elseif (file_exists($packagesdir . '/' . $gzfilename . '/package-info.xml'))
542
			$packageInfo = file_get_contents($packagesdir . '/' . $gzfilename . '/package-info.xml');
543
		else
544
			return 'package_get_error_missing_xml';
545
	}
546
547
	// Nothing?
548
	if (empty($packageInfo))
549
	{
550
		// Perhaps they are trying to install a theme, lets tell them nicely this is the wrong function
551
		$packageInfo = read_tgz_file($packagesdir . '/' . $gzfilename, '*/theme_info.xml', true);
552
		if (!empty($packageInfo))
553
			return 'package_get_error_is_theme';
554
		else
555
			return 'package_get_error_is_zero';
556
	}
557
558
	// Parse package-info.xml into an xmlArray.
559
	require_once($sourcedir . '/Class-Package.php');
560
	$packageInfo = new xmlArray($packageInfo);
0 ignored issues
show
Bug introduced by
It seems like $packageInfo defined by new \xmlArray($packageInfo) on line 560 can also be of type array; however, xmlArray::__construct() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
561
562
	// @todo Error message of some sort?
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
563
	if (!$packageInfo->exists('package-info[0]'))
564
		return 'package_get_error_packageinfo_corrupt';
565
566
	$packageInfo = $packageInfo->path('package-info[0]');
567
568
	$package = $packageInfo->to_array();
569
	$package = htmlspecialchars__recursive($package);
570
	$package['xml'] = $packageInfo;
571
	$package['filename'] = $gzfilename;
572
573
	// Don't want to mess with code...
574
	$types = array('install', 'uninstall', 'upgrade');
575
	foreach ($types as $type)
576
	{
577
		if (isset($package[$type]['code']))
578
		{
579
			$package[$type]['code'] = un_htmlspecialchars($package[$type]['code']);
580
		}
581
	}
582
583
	if (!isset($package['type']))
584
		$package['type'] = 'modification';
585
586
	return $package;
587
}
588
589
/**
590
 * Create a chmod control for chmoding files.
591
 *
592
 * @param array $chmodFiles Which files to chmod
593
 * @param array $chmodOptions Options for chmod
594
 * @param bool $restore_write_status Whether to restore write status
595
 * @return array An array of file info
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean|array<string,array<string,array>>?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
596
 */
597
function create_chmod_control($chmodFiles = array(), $chmodOptions = array(), $restore_write_status = false)
598
{
599
	global $context, $modSettings, $package_ftp, $boarddir, $txt, $sourcedir, $scripturl;
600
601
	// If we're restoring the status of existing files prepare the data.
602
	if ($restore_write_status && isset($_SESSION['pack_ftp']) && !empty($_SESSION['pack_ftp']['original_perms']))
603
	{
604
		/**
605
		 * Get a listing of files that will need to be set back to the original state
606
		 *
607
		 * @param null $dummy1
608
		 * @param null $dummy2
609
		 * @param null $dummy3
610
		 * @param bool $do_change
611
		 * @return array An array of info about the files that need to be restored back to their original state
612
		 */
613
		function list_restoreFiles($dummy1, $dummy2, $dummy3, $do_change)
0 ignored issues
show
Unused Code introduced by
The parameter $dummy1 is not used and could be removed.

This check looks from 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.

This check looks from 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.

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

Loading history...
614
		{
615
			global $txt;
616
617
			$restore_files = array();
618
			foreach ($_SESSION['pack_ftp']['original_perms'] as $file => $perms)
619
			{
620
				// Check the file still exists, and the permissions were indeed different than now.
621
				$file_permissions = @fileperms($file);
622
				if (!file_exists($file) || $file_permissions == $perms)
623
				{
624
					unset($_SESSION['pack_ftp']['original_perms'][$file]);
625
					continue;
626
				}
627
628
				// Are we wanting to change the permission?
629
				if ($do_change && isset($_POST['restore_files']) && in_array($file, $_POST['restore_files']))
630
				{
631
					// Use FTP if we have it.
632
					// @todo where does $package_ftp get set?
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
633 View Code Duplication
					if (!empty($package_ftp))
634
					{
635
						$ftp_file = strtr($file, array($_SESSION['pack_ftp']['root'] => ''));
636
						$package_ftp->chmod($ftp_file, $perms);
637
					}
638
					else
639
						smf_chmod($file, $perms);
640
641
					$new_permissions = @fileperms($file);
642
					$result = $new_permissions == $perms ? 'success' : 'failure';
643
					unset($_SESSION['pack_ftp']['original_perms'][$file]);
644
				}
645
				elseif ($do_change)
646
				{
647
					$new_permissions = '';
648
					$result = 'skipped';
649
					unset($_SESSION['pack_ftp']['original_perms'][$file]);
650
				}
651
652
				// Record the results!
653
				$restore_files[] = array(
654
					'path' => $file,
655
					'old_perms_raw' => $perms,
656
					'old_perms' => substr(sprintf('%o', $perms), -4),
657
					'cur_perms' => substr(sprintf('%o', $file_permissions), -4),
658
					'new_perms' => isset($new_permissions) ? substr(sprintf('%o', $new_permissions), -4) : '',
659
					'result' => isset($result) ? $result : '',
660
					'writable_message' => '<span style="color: ' . (@is_writable($file) ? 'green' : 'red') . '">' . (@is_writable($file) ? $txt['package_file_perms_writable'] : $txt['package_file_perms_not_writable']) . '</span>',
661
				);
662
			}
663
664
			return $restore_files;
665
		}
666
667
		$listOptions = array(
668
			'id' => 'restore_file_permissions',
669
			'title' => $txt['package_restore_permissions'],
670
			'get_items' => array(
671
				'function' => 'list_restoreFiles',
672
				'params' => array(
673
					!empty($_POST['restore_perms']),
674
				),
675
			),
676
			'columns' => array(
677
				'path' => array(
678
					'header' => array(
679
						'value' => $txt['package_restore_permissions_filename'],
680
					),
681
					'data' => array(
682
						'db' => 'path',
683
						'class' => 'smalltext',
684
					),
685
				),
686
				'old_perms' => array(
687
					'header' => array(
688
						'value' => $txt['package_restore_permissions_orig_status'],
689
					),
690
					'data' => array(
691
						'db' => 'old_perms',
692
						'class' => 'smalltext',
693
					),
694
				),
695
				'cur_perms' => array(
696
					'header' => array(
697
						'value' => $txt['package_restore_permissions_cur_status'],
698
					),
699
					'data' => array(
700
						'function' => function($rowData) use ($txt)
701
						{
702
							$formatTxt = $rowData['result'] == '' || $rowData['result'] == 'skipped' ? $txt['package_restore_permissions_pre_change'] : $txt['package_restore_permissions_post_change'];
703
							return sprintf($formatTxt, $rowData['cur_perms'], $rowData['new_perms'], $rowData['writable_message']);
704
						},
705
						'class' => 'smalltext',
706
					),
707
				),
708
				'check' => array(
709
					'header' => array(
710
						'value' => '<input type="checkbox" onclick="invertAll(this, this.form);" class="input_check">',
711
						'class' => 'centercol',
712
					),
713
					'data' => array(
714
						'sprintf' => array(
715
							'format' => '<input type="checkbox" name="restore_files[]" value="%1$s" class="input_check">',
716
							'params' => array(
717
								'path' => false,
718
							),
719
						),
720
						'class' => 'centercol',
721
					),
722
				),
723
				'result' => array(
724
					'header' => array(
725
						'value' => $txt['package_restore_permissions_result'],
726
					),
727
					'data' => array(
728
						'function' => function($rowData) use ($txt)
729
						{
730
							return $txt['package_restore_permissions_action_' . $rowData['result']];
731
						},
732
						'class' => 'smalltext',
733
					),
734
				),
735
			),
736
			'form' => array(
737
				'href' => !empty($chmodOptions['destination_url']) ? $chmodOptions['destination_url'] : $scripturl . '?action=admin;area=packages;sa=perms;restore;' . $context['session_var'] . '=' . $context['session_id'],
738
			),
739
			'additional_rows' => array(
740
				array(
741
					'position' => 'below_table_data',
742
					'value' => '<input type="submit" name="restore_perms" value="' . $txt['package_restore_permissions_restore'] . '" class="button_submit">',
743
					'class' => 'titlebg',
744
				),
745
				array(
746
					'position' => 'after_title',
747
					'value' => '<span class="smalltext">' . $txt['package_restore_permissions_desc'] . '</span>',
748
					'class' => 'windowbg2',
749
				),
750
			),
751
		);
752
753
		// Work out what columns and the like to show.
754
		if (!empty($_POST['restore_perms']))
755
		{
756
			$listOptions['additional_rows'][1]['value'] = sprintf($txt['package_restore_permissions_action_done'], $scripturl . '?action=admin;area=packages;sa=perms;' . $context['session_var'] . '=' . $context['session_id']);
757
			unset($listOptions['columns']['check'], $listOptions['form'], $listOptions['additional_rows'][0]);
758
759
			$context['sub_template'] = 'show_list';
760
			$context['default_list'] = 'restore_file_permissions';
761
		}
762
		else
763
		{
764
			unset($listOptions['columns']['result']);
765
		}
766
767
		// Create the list for display.
768
		require_once($sourcedir . '/Subs-List.php');
769
		createList($listOptions);
770
771
		// If we just restored permissions then whereever we are, we are now done and dusted.
772
		if (!empty($_POST['restore_perms']))
773
			obExit();
774
	}
775
	// Otherwise, it's entirely irrelevant?
776
	elseif ($restore_write_status)
777
		return true;
778
779
	// This is where we report what we got up to.
780
	$return_data = array(
781
		'files' => array(
782
			'writable' => array(),
783
			'notwritable' => array(),
784
		),
785
	);
786
787
	// If we have some FTP information already, then let's assume it was required and try to get ourselves connected.
788
	if (!empty($_SESSION['pack_ftp']['connected']))
789
	{
790
		// Load the file containing the ftp_connection class.
791
		require_once($sourcedir . '/Class-Package.php');
792
793
		$package_ftp = new ftp_connection($_SESSION['pack_ftp']['server'], $_SESSION['pack_ftp']['port'], $_SESSION['pack_ftp']['username'], package_crypt($_SESSION['pack_ftp']['password']));
794
	}
795
796
	// Just got a submission did we?
797
	if (empty($package_ftp) && isset($_POST['ftp_username']))
798
	{
799
		require_once($sourcedir . '/Class-Package.php');
800
		$ftp = new ftp_connection($_POST['ftp_server'], $_POST['ftp_port'], $_POST['ftp_username'], $_POST['ftp_password']);
801
802
		// We're connected, jolly good!
803
		if ($ftp->error === false)
804
		{
805
			// Common mistake, so let's try to remedy it...
806
			if (!$ftp->chdir($_POST['ftp_path']))
807
			{
808
				$ftp_error = $ftp->last_message;
809
				$ftp->chdir(preg_replace('~^/home[2]?/[^/]+?~', '', $_POST['ftp_path']));
810
			}
811
812 View Code Duplication
			if (!in_array($_POST['ftp_path'], array('', '/')))
813
			{
814
				$ftp_root = strtr($boarddir, array($_POST['ftp_path'] => ''));
815
				if (substr($ftp_root, -1) == '/' && ($_POST['ftp_path'] == '' || substr($_POST['ftp_path'], 0, 1) == '/'))
816
					$ftp_root = substr($ftp_root, 0, -1);
817
			}
818
			else
819
				$ftp_root = $boarddir;
820
821
			$_SESSION['pack_ftp'] = array(
822
				'server' => $_POST['ftp_server'],
823
				'port' => $_POST['ftp_port'],
824
				'username' => $_POST['ftp_username'],
825
				'password' => package_crypt($_POST['ftp_password']),
826
				'path' => $_POST['ftp_path'],
827
				'root' => $ftp_root,
828
				'connected' => true,
829
			);
830
831 View Code Duplication
			if (!isset($modSettings['package_path']) || $modSettings['package_path'] != $_POST['ftp_path'])
832
				updateSettings(array('package_path' => $_POST['ftp_path']));
833
834
			// This is now the primary connection.
835
			$package_ftp = $ftp;
836
		}
837
	}
838
839
	// Now try to simply make the files writable, with whatever we might have.
840
	if (!empty($chmodFiles))
841
	{
842
		foreach ($chmodFiles as $k => $file)
843
		{
844
			// Sometimes this can somehow happen maybe?
845
			if (empty($file))
846
				unset($chmodFiles[$k]);
847
			// Already writable?
848
			elseif (@is_writable($file))
849
				$return_data['files']['writable'][] = $file;
850
			else
851
			{
852
				// Now try to change that.
853
				$return_data['files'][package_chmod($file, 'writable', true) ? 'writable' : 'notwritable'][] = $file;
854
			}
855
		}
856
	}
857
858
	// Have we still got nasty files which ain't writable? Dear me we need more FTP good sir.
859
	if (empty($package_ftp) && (!empty($return_data['files']['notwritable']) || !empty($chmodOptions['force_find_error'])))
860
	{
861
		if (!isset($ftp) || $ftp->error !== false)
862
		{
863 View Code Duplication
			if (!isset($ftp))
864
			{
865
				require_once($sourcedir . '/Class-Package.php');
866
				$ftp = new ftp_connection(null);
867
			}
868
			elseif ($ftp->error !== false && !isset($ftp_error))
869
				$ftp_error = $ftp->last_message === null ? '' : $ftp->last_message;
870
871
			list ($username, $detect_path, $found_path) = $ftp->detect_path($boarddir);
872
873 View Code Duplication
			if ($found_path)
874
				$_POST['ftp_path'] = $detect_path;
875
			elseif (!isset($_POST['ftp_path']))
876
				$_POST['ftp_path'] = isset($modSettings['package_path']) ? $modSettings['package_path'] : $detect_path;
877
878
			if (!isset($_POST['ftp_username']))
879
				$_POST['ftp_username'] = $username;
880
		}
881
882
		$context['package_ftp'] = array(
883
			'server' => isset($_POST['ftp_server']) ? $_POST['ftp_server'] : (isset($modSettings['package_server']) ? $modSettings['package_server'] : 'localhost'),
884
			'port' => isset($_POST['ftp_port']) ? $_POST['ftp_port'] : (isset($modSettings['package_port']) ? $modSettings['package_port'] : '21'),
885
			'username' => isset($_POST['ftp_username']) ? $_POST['ftp_username'] : (isset($modSettings['package_username']) ? $modSettings['package_username'] : ''),
886
			'path' => $_POST['ftp_path'],
887
			'error' => empty($ftp_error) ? null : $ftp_error,
888
			'destination' => !empty($chmodOptions['destination_url']) ? $chmodOptions['destination_url'] : '',
889
		);
890
891
		// Which files failed?
892
		if (!isset($context['notwritable_files']))
893
			$context['notwritable_files'] = array();
894
		$context['notwritable_files'] = array_merge($context['notwritable_files'], $return_data['files']['notwritable']);
895
896
		// Sent here to die?
897
		if (!empty($chmodOptions['crash_on_error']))
898
		{
899
			$context['page_title'] = $txt['package_ftp_necessary'];
900
			$context['sub_template'] = 'ftp_required';
901
			obExit();
902
		}
903
	}
904
905
	return $return_data;
906
}
907
908
/**
909
 * Use FTP functions to work with a package download/install
910
 *
911
 * @param string $destination_url The destination URL
912
 * @param null|array $files The files to CHMOD
913
 * @param bool $return Whether to return an array of file info if there's an error
914
 * @return array An array of file info
0 ignored issues
show
Documentation introduced by
Should the return type not be array|null? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
915
 */
916
function packageRequireFTP($destination_url, $files = null, $return = false)
917
{
918
	global $context, $modSettings, $package_ftp, $boarddir, $txt, $sourcedir;
919
920
	// Try to make them writable the manual way.
921
	if ($files !== null)
922
	{
923
		foreach ($files as $k => $file)
924
		{
925
			// If this file doesn't exist, then we actually want to look at the directory, no?
926
			if (!file_exists($file))
927
				$file = dirname($file);
928
929
			// This looks odd, but it's an attempt to work around PHP suExec.
930
			if (!@is_writable($file))
931
				smf_chmod($file, 0755);
932
			if (!@is_writable($file))
933
				smf_chmod($file, 0777);
934
			if (!@is_writable(dirname($file)))
935
				smf_chmod($file, 0755);
936
			if (!@is_writable(dirname($file)))
937
				smf_chmod($file, 0777);
938
939
			$fp = is_dir($file) ? @opendir($file) : @fopen($file, 'rb');
940
			if (@is_writable($file) && $fp)
941
			{
942
				unset($files[$k]);
943
				if (!is_dir($file))
944
					fclose($fp);
945
				else
946
					closedir($fp);
947
			}
948
		}
949
950
		// No FTP required!
951
		if (empty($files))
952
			return array();
953
	}
954
955
	// They've opted to not use FTP, and try anyway.
956
	if (isset($_SESSION['pack_ftp']) && $_SESSION['pack_ftp'] == false)
957
	{
958
		if ($files === null)
959
			return array();
960
961
		foreach ($files as $k => $file)
962
		{
963
			// This looks odd, but it's an attempt to work around PHP suExec.
964
			if (!file_exists($file))
965
			{
966
				mktree(dirname($file), 0755);
967
				@touch($file);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
968
				smf_chmod($file, 0755);
969
			}
970
971
			if (!@is_writable($file))
972
				smf_chmod($file, 0777);
973
			if (!@is_writable(dirname($file)))
974
				smf_chmod(dirname($file), 0777);
975
976
			if (@is_writable($file))
977
				unset($files[$k]);
978
		}
979
980
		return $files;
981
	}
982
	elseif (isset($_SESSION['pack_ftp']))
983
	{
984
		// Load the file containing the ftp_connection class.
985
		require_once($sourcedir . '/Class-Package.php');
986
987
		$package_ftp = new ftp_connection($_SESSION['pack_ftp']['server'], $_SESSION['pack_ftp']['port'], $_SESSION['pack_ftp']['username'], package_crypt($_SESSION['pack_ftp']['password']));
988
989
		if ($files === null)
990
			return array();
991
992
		foreach ($files as $k => $file)
993
		{
994
			$ftp_file = strtr($file, array($_SESSION['pack_ftp']['root'] => ''));
995
996
			// This looks odd, but it's an attempt to work around PHP suExec.
997
			if (!file_exists($file))
998
			{
999
				mktree(dirname($file), 0755);
1000
				$package_ftp->create_file($ftp_file);
1001
				$package_ftp->chmod($ftp_file, 0755);
1002
			}
1003
1004
			if (!@is_writable($file))
1005
				$package_ftp->chmod($ftp_file, 0777);
1006
			if (!@is_writable(dirname($file)))
1007
				$package_ftp->chmod(dirname($ftp_file), 0777);
1008
1009
			if (@is_writable($file))
1010
				unset($files[$k]);
1011
		}
1012
1013
		return $files;
1014
	}
1015
1016
	if (isset($_POST['ftp_none']))
1017
	{
1018
		$_SESSION['pack_ftp'] = false;
1019
1020
		$files = packageRequireFTP($destination_url, $files, $return);
1021
		return $files;
1022
	}
1023 View Code Duplication
	elseif (isset($_POST['ftp_username']))
1024
	{
1025
		require_once($sourcedir . '/Class-Package.php');
1026
		$ftp = new ftp_connection($_POST['ftp_server'], $_POST['ftp_port'], $_POST['ftp_username'], $_POST['ftp_password']);
1027
1028
		if ($ftp->error === false)
1029
		{
1030
			// Common mistake, so let's try to remedy it...
1031
			if (!$ftp->chdir($_POST['ftp_path']))
1032
			{
1033
				$ftp_error = $ftp->last_message;
1034
				$ftp->chdir(preg_replace('~^/home[2]?/[^/]+?~', '', $_POST['ftp_path']));
1035
			}
1036
		}
1037
	}
1038
1039
	if (!isset($ftp) || $ftp->error !== false)
1040
	{
1041 View Code Duplication
		if (!isset($ftp))
1042
		{
1043
			require_once($sourcedir . '/Class-Package.php');
1044
			$ftp = new ftp_connection(null);
1045
		}
1046
		elseif ($ftp->error !== false && !isset($ftp_error))
1047
			$ftp_error = $ftp->last_message === null ? '' : $ftp->last_message;
1048
1049
		list ($username, $detect_path, $found_path) = $ftp->detect_path($boarddir);
1050
1051 View Code Duplication
		if ($found_path)
1052
			$_POST['ftp_path'] = $detect_path;
1053
		elseif (!isset($_POST['ftp_path']))
1054
			$_POST['ftp_path'] = isset($modSettings['package_path']) ? $modSettings['package_path'] : $detect_path;
1055
1056
		if (!isset($_POST['ftp_username']))
1057
			$_POST['ftp_username'] = $username;
1058
1059
		$context['package_ftp'] = array(
1060
			'server' => isset($_POST['ftp_server']) ? $_POST['ftp_server'] : (isset($modSettings['package_server']) ? $modSettings['package_server'] : 'localhost'),
1061
			'port' => isset($_POST['ftp_port']) ? $_POST['ftp_port'] : (isset($modSettings['package_port']) ? $modSettings['package_port'] : '21'),
1062
			'username' => isset($_POST['ftp_username']) ? $_POST['ftp_username'] : (isset($modSettings['package_username']) ? $modSettings['package_username'] : ''),
1063
			'path' => $_POST['ftp_path'],
1064
			'error' => empty($ftp_error) ? null : $ftp_error,
1065
			'destination' => $destination_url,
1066
		);
1067
1068
		// If we're returning dump out here.
1069
		if ($return)
1070
			return $files;
1071
1072
		$context['page_title'] = $txt['package_ftp_necessary'];
1073
		$context['sub_template'] = 'ftp_required';
1074
		obExit();
1075
	}
1076
	else
1077
	{
1078 View Code Duplication
		if (!in_array($_POST['ftp_path'], array('', '/')))
1079
		{
1080
			$ftp_root = strtr($boarddir, array($_POST['ftp_path'] => ''));
1081
			if (substr($ftp_root, -1) == '/' && ($_POST['ftp_path'] == '' || $_POST['ftp_path'][0] == '/'))
1082
				$ftp_root = substr($ftp_root, 0, -1);
1083
		}
1084
		else
1085
			$ftp_root = $boarddir;
1086
1087
		$_SESSION['pack_ftp'] = array(
1088
			'server' => $_POST['ftp_server'],
1089
			'port' => $_POST['ftp_port'],
1090
			'username' => $_POST['ftp_username'],
1091
			'password' => package_crypt($_POST['ftp_password']),
1092
			'path' => $_POST['ftp_path'],
1093
			'root' => $ftp_root,
1094
		);
1095
1096 View Code Duplication
		if (!isset($modSettings['package_path']) || $modSettings['package_path'] != $_POST['ftp_path'])
1097
			updateSettings(array('package_path' => $_POST['ftp_path']));
1098
1099
		$files = packageRequireFTP($destination_url, $files, $return);
1100
	}
1101
1102
	return $files;
1103
}
1104
1105
/**
1106
 * Parses the actions in package-info.xml file from packages.
1107
 *
1108
 * - package should be an xmlArray with package-info as its base.
1109
 * - testing_only should be true if the package should not actually be applied.
1110
 * - method can be upgrade, install, or uninstall.  Its default is install.
1111
 * - previous_version should be set to the previous installed version of this package, if any.
1112
 * - does not handle failure terribly well; testing first is always better.
1113
 *
1114
 * @param xmlArray &$packageXML The info from the package-info file
1115
 * @param bool $testing_only Whether we're only testing
1116
 * @param string $method The method ('install', 'upgrade', or 'uninstall')
1117
 * @param string $previous_version The previous version of the mod, if method is 'upgrade'
1118
 * @return array An array of those changes made.
1119
 */
1120
function parsePackageInfo(&$packageXML, $testing_only = true, $method = 'install', $previous_version = '')
1121
{
1122
	global $packagesdir, $forum_version, $context, $temp_path, $language, $smcFunc;
1123
1124
	// Mayday!  That action doesn't exist!!
1125
	if (empty($packageXML) || !$packageXML->exists($method))
1126
		return array();
1127
1128
	// We haven't found the package script yet...
1129
	$script = false;
1130
	$the_version = strtr($forum_version, array('SMF ' => ''));
1131
1132
	// Emulation support...
1133
	if (!empty($_SESSION['version_emulate']))
1134
		$the_version = $_SESSION['version_emulate'];
1135
1136
	// Single package emulation
1137 View Code Duplication
	if (!empty($_REQUEST['ve']) && !empty($_REQUEST['package']))
1138
	{
1139
		$the_version = $_REQUEST['ve'];
1140
		$_SESSION['single_version_emulate'][$_REQUEST['package']] = $the_version;
1141
	}
1142 View Code Duplication
	if (!empty($_REQUEST['package']) && (!empty($_SESSION['single_version_emulate'][$_REQUEST['package']])))
1143
		$the_version = $_SESSION['single_version_emulate'][$_REQUEST['package']];
1144
1145
	// Get all the versions of this method and find the right one.
1146
	$these_methods = $packageXML->set($method);
1147
	foreach ($these_methods as $this_method)
1148
	{
1149
		// They specified certain versions this part is for.
1150
		if ($this_method->exists('@for'))
1151
		{
1152
			// Don't keep going if this won't work for this version of SMF.
1153
			if (!matchPackageVersion($the_version, $this_method->fetch('@for')))
1154
				continue;
1155
		}
1156
1157
		// Upgrades may go from a certain old version of the mod.
1158
		if ($method == 'upgrade' && $this_method->exists('@from'))
1159
		{
1160
			// Well, this is for the wrong old version...
1161
			if (!matchPackageVersion($previous_version, $this_method->fetch('@from')))
1162
				continue;
1163
		}
1164
1165
		// We've found it!
1166
		$script = $this_method;
1167
		break;
1168
	}
1169
1170
	// Bad news, a matching script wasn't found!
1171
	if (!($script instanceof xmlArray))
1172
		return array();
1173
1174
	// Find all the actions in this method - in theory, these should only be allowed actions. (* means all.)
1175
	$actions = $script->set('*');
1176
	$return = array();
1177
1178
	$temp_auto = 0;
1179
	$temp_path = $packagesdir . '/temp/' . (isset($context['base_path']) ? $context['base_path'] : '');
1180
1181
	$context['readmes'] = array();
1182
	$context['licences'] = array();
1183
1184
	// This is the testing phase... nothing shall be done yet.
1185
	foreach ($actions as $action)
1186
	{
1187
		$actionType = $action->name();
1188
1189
		if (in_array($actionType, array('readme', 'code', 'database', 'modification', 'redirect', 'license')))
1190
		{
1191
			// Allow for translated readme and license files.
1192
			if ($actionType == 'readme' || $actionType == 'license')
1193
			{
1194
				$type = $actionType . 's';
1195
				if ($action->exists('@lang'))
1196
				{
1197
					// Auto-select the language based on either request variable or current language.
1198
					if ((isset($_REQUEST['readme']) && $action->fetch('@lang') == $_REQUEST['readme']) || (isset($_REQUEST['license']) && $action->fetch('@lang') == $_REQUEST['license']) || (!isset($_REQUEST['readme']) && $action->fetch('@lang') == $language) || (!isset($_REQUEST['license']) && $action->fetch('@lang') == $language))
1199
					{
1200
						// In case the user put the blocks in the wrong order.
1201 View Code Duplication
						if (isset($context[$type]['selected']) && $context[$type]['selected'] == 'default')
1202
							$context[$type][] = 'default';
1203
1204
						$context[$type]['selected'] = $smcFunc['htmlspecialchars']($action->fetch('@lang'));
1205
					}
1206
					else
1207
					{
1208
						// We don't want this now, but we'll allow the user to select to read it.
1209
						$context[$type][] = $smcFunc['htmlspecialchars']($action->fetch('@lang'));
1210
						continue;
1211
					}
1212
				}
1213
				// Fallback when we have no lang parameter.
1214 View Code Duplication
				else
1215
				{
1216
					// Already selected one for use?
1217
					if (isset($context[$type]['selected']))
1218
					{
1219
						$context[$type][] = 'default';
1220
						continue;
1221
					}
1222
					else
1223
						$context[$type]['selected'] = 'default';
1224
				}
1225
			}
1226
1227
			// @todo Make sure the file actually exists?  Might not work when testing?
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
1228
			if ($action->exists('@type') && $action->fetch('@type') == 'inline')
1229
			{
1230
				$filename = $temp_path . '$auto_' . $temp_auto++ . (in_array($actionType, array('readme', 'redirect', 'license')) ? '.txt' : ($actionType == 'code' || $actionType == 'database' ? '.php' : '.mod'));
0 ignored issues
show
Coding Style introduced by
Increment and decrement operators must be bracketed when used in string concatenation
Loading history...
1231
				package_put_contents($filename, $action->fetch('.'));
0 ignored issues
show
Security Bug introduced by
It seems like $action->fetch('.') targeting xmlArray::fetch() can also be of type false; however, package_put_contents() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
1232
				$filename = strtr($filename, array($temp_path => ''));
1233
			}
1234
			else
1235
				$filename = $action->fetch('.');
1236
1237
			$return[] = array(
1238
				'type' => $actionType,
1239
				'filename' => $filename,
1240
				'description' => '',
1241
				'reverse' => $action->exists('@reverse') && $action->fetch('@reverse') == 'true',
1242
				'boardmod' => $action->exists('@format') && $action->fetch('@format') == 'boardmod',
1243
				'redirect_url' => $action->exists('@url') ? $action->fetch('@url') : '',
1244
				'redirect_timeout' => $action->exists('@timeout') ? (int) $action->fetch('@timeout') : '',
1245
				'parse_bbc' => $action->exists('@parsebbc') && $action->fetch('@parsebbc') == 'true',
1246
				'language' => (($actionType == 'readme' || $actionType == 'license') && $action->exists('@lang') && $action->fetch('@lang') == $language) ? $language : '',
1247
			);
1248
1249
			continue;
1250
		}
1251
		elseif ($actionType == 'hook')
1252
		{
1253
			$return[] = array(
1254
				'type' => $actionType,
1255
				'function' => $action->exists('@function') ? $action->fetch('@function') : '',
1256
				'hook' => $action->exists('@hook') ? $action->fetch('@hook') : $action->fetch('.'),
1257
				'include_file' => $action->exists('@file') ? $action->fetch('@file') : '',
1258
				'reverse' => $action->exists('@reverse') && $action->fetch('@reverse') == 'true' ? true : false,
1259
				'object' => $action->exists('@object') && $action->fetch('@object') == 'true' ? true : false,
1260
				'description' => '',
1261
			);
1262
			continue;
1263
		}
1264
		elseif ($actionType == 'credits')
1265
		{
1266
			// quick check of any supplied url
1267
			$url = $action->exists('@url') ? $action->fetch('@url') : '';
1268
			if (strlen(trim($url)) > 0 && substr($url, 0, 7) !== 'http://' && substr($url, 0, 8) !== 'https://')
1269
			{
1270
				$url = 'http://' . $url;
1271 View Code Duplication
				if (strlen($url) < 8 || (substr($url, 0, 7) !== 'http://' && substr($url, 0, 8) !== 'https://'))
1272
					$url = '';
1273
			}
1274
1275
			$return[] = array(
1276
				'type' => $actionType,
1277
				'url' => $url,
1278
				'license' => $action->exists('@license') ? $action->fetch('@license') : '',
1279
				'licenseurl' => $action->exists('@licenseurl') ? $action->fetch('@licenseurl') : '',
1280
				'copyright' => $action->exists('@copyright') ? $action->fetch('@copyright') : '',
1281
				'title' => $action->fetch('.'),
1282
			);
1283
			continue;
1284
		}
1285
		elseif ($actionType == 'requires')
1286
		{
1287
			$return[] = array(
1288
				'type' => $actionType,
1289
				'id' => $action->exists('@id') ? $action->fetch('@id') : '',
1290
				'version' => $action->exists('@version') ? $action->fetch('@version') : $action->fetch('.'),
1291
				'description' => '',
1292
			);
1293
			continue;
1294
		}
1295
		elseif ($actionType == 'error')
1296
		{
1297
			$return[] = array(
1298
				'type' => 'error',
1299
			);
1300
		}
1301
		elseif (in_array($actionType, array('require-file', 'remove-file', 'require-dir', 'remove-dir', 'move-file', 'move-dir', 'create-file', 'create-dir')))
1302
		{
1303
			$this_action = &$return[];
1304
			$this_action = array(
1305
				'type' => $actionType,
1306
				'filename' => $action->fetch('@name'),
1307
				'description' => $action->fetch('.')
1308
			);
1309
1310
			// If there is a destination, make sure it makes sense.
1311
			if (substr($actionType, 0, 6) != 'remove')
1312
			{
1313
				$this_action['unparsed_destination'] = $action->fetch('@destination');
1314
				$this_action['destination'] = parse_path($action->fetch('@destination')) . '/' . basename($this_action['filename']);
0 ignored issues
show
Security Bug introduced by
It seems like $action->fetch('@destination') targeting xmlArray::fetch() can also be of type false; however, parse_path() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
1315
			}
1316
			else
1317
			{
1318
				$this_action['unparsed_filename'] = $this_action['filename'];
1319
				$this_action['filename'] = parse_path($this_action['filename']);
1320
			}
1321
1322
			// If we're moving or requiring (copying) a file.
1323
			if (substr($actionType, 0, 4) == 'move' || substr($actionType, 0, 7) == 'require')
1324
			{
1325
				if ($action->exists('@from'))
1326
					$this_action['source'] = parse_path($action->fetch('@from'));
0 ignored issues
show
Security Bug introduced by
It seems like $action->fetch('@from') targeting xmlArray::fetch() can also be of type false; however, parse_path() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
1327
				else
1328
					$this_action['source'] = $temp_path . $this_action['filename'];
1329
			}
1330
1331
			// Check if these things can be done. (chmod's etc.)
1332
			if ($actionType == 'create-dir')
1333
			{
1334
				if (!mktree($this_action['destination'], false))
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Security Bug introduced by
It seems like $this_action['destination'] can also be of type false; however, mktree() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
1335
				{
1336
					$temp = $this_action['destination'];
1337
					while (!file_exists($temp) && strlen($temp) > 1)
1338
						$temp = dirname($temp);
1339
1340
					$return[] = array(
1341
						'type' => 'chmod',
1342
						'filename' => $temp
1343
					);
1344
				}
1345
			}
1346 View Code Duplication
			elseif ($actionType == 'create-file')
1347
			{
1348
				if (!mktree(dirname($this_action['destination']), false))
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1349
				{
1350
					$temp = dirname($this_action['destination']);
1351
					while (!file_exists($temp) && strlen($temp) > 1)
1352
						$temp = dirname($temp);
1353
1354
					$return[] = array(
1355
						'type' => 'chmod',
1356
						'filename' => $temp
1357
					);
1358
				}
1359
1360
				if (!is_writable($this_action['destination']) && (file_exists($this_action['destination']) || !is_writable(dirname($this_action['destination']))))
1361
					$return[] = array(
1362
						'type' => 'chmod',
1363
						'filename' => $this_action['destination']
1364
					);
1365
			}
1366
			elseif ($actionType == 'require-dir')
1367
			{
1368
				if (!mktree($this_action['destination'], false))
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Security Bug introduced by
It seems like $this_action['destination'] can also be of type false; however, mktree() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
1369
				{
1370
					$temp = $this_action['destination'];
1371
					while (!file_exists($temp) && strlen($temp) > 1)
1372
						$temp = dirname($temp);
1373
1374
					$return[] = array(
1375
						'type' => 'chmod',
1376
						'filename' => $temp
1377
					);
1378
				}
1379
			}
1380
			elseif ($actionType == 'require-file')
1381
			{
1382
				if ($action->exists('@theme'))
1383
					$this_action['theme_action'] = $action->fetch('@theme');
1384
1385
				if (!mktree(dirname($this_action['destination']), false))
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1386
				{
1387
					$temp = dirname($this_action['destination']);
1388
					while (!file_exists($temp) && strlen($temp) > 1)
1389
						$temp = dirname($temp);
1390
1391
					$return[] = array(
1392
						'type' => 'chmod',
1393
						'filename' => $temp
1394
					);
1395
				}
1396
1397
				if (!is_writable($this_action['destination']) && (file_exists($this_action['destination']) || !is_writable(dirname($this_action['destination']))))
1398
					$return[] = array(
1399
						'type' => 'chmod',
1400
						'filename' => $this_action['destination']
1401
					);
1402
			}
1403 View Code Duplication
			elseif ($actionType == 'move-dir' || $actionType == 'move-file')
1404
			{
1405
				if (!mktree(dirname($this_action['destination']), false))
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1406
				{
1407
					$temp = dirname($this_action['destination']);
1408
					while (!file_exists($temp) && strlen($temp) > 1)
1409
						$temp = dirname($temp);
1410
1411
					$return[] = array(
1412
						'type' => 'chmod',
1413
						'filename' => $temp
1414
					);
1415
				}
1416
1417
				if (!is_writable($this_action['destination']) && (file_exists($this_action['destination']) || !is_writable(dirname($this_action['destination']))))
1418
					$return[] = array(
1419
						'type' => 'chmod',
1420
						'filename' => $this_action['destination']
1421
					);
1422
			}
1423 View Code Duplication
			elseif ($actionType == 'remove-dir')
1424
			{
1425
				if (!is_writable($this_action['filename']) && file_exists($this_action['filename']))
1426
					$return[] = array(
1427
						'type' => 'chmod',
1428
						'filename' => $this_action['filename']
1429
					);
1430
			}
1431 View Code Duplication
			elseif ($actionType == 'remove-file')
1432
			{
1433
				if (!is_writable($this_action['filename']) && file_exists($this_action['filename']))
1434
					$return[] = array(
1435
						'type' => 'chmod',
1436
						'filename' => $this_action['filename']
1437
					);
1438
			}
1439
		}
1440
		else
1441
		{
1442
			$return[] = array(
1443
				'type' => 'error',
1444
				'error_msg' => 'unknown_action',
1445
				'error_var' => $actionType
1446
			);
1447
		}
1448
	}
1449
1450
	// Only testing - just return a list of things to be done.
1451
	if ($testing_only)
1452
		return $return;
1453
1454
	umask(0);
1455
1456
	$failure = false;
1457
	$not_done = array(array('type' => '!'));
1458
	foreach ($return as $action)
1459
	{
1460
		if (in_array($action['type'], array('modification', 'code', 'database', 'redirect', 'hook', 'credits')))
1461
			$not_done[] = $action;
1462
1463
		if ($action['type'] == 'create-dir')
1464
		{
1465 View Code Duplication
			if (!mktree($action['destination'], 0755) || !is_writable($action['destination']))
1466
				$failure |= !mktree($action['destination'], 0777);
1467
		}
1468
		elseif ($action['type'] == 'create-file')
1469
		{
1470 View Code Duplication
			if (!mktree(dirname($action['destination']), 0755) || !is_writable(dirname($action['destination'])))
1471
				$failure |= !mktree(dirname($action['destination']), 0777);
1472
1473
			// Create an empty file.
1474
			package_put_contents($action['destination'], package_get_contents($action['source']), $testing_only);
1475
1476
			if (!file_exists($action['destination']))
1477
				$failure = true;
1478
		}
1479
		elseif ($action['type'] == 'require-dir')
1480
		{
1481
			copytree($action['source'], $action['destination']);
1482
			// Any other theme folders?
1483
			if (!empty($context['theme_copies']) && !empty($context['theme_copies'][$action['type']][$action['destination']]))
1484
				foreach ($context['theme_copies'][$action['type']][$action['destination']] as $theme_destination)
1485
					copytree($action['source'], $theme_destination);
1486
		}
1487
		elseif ($action['type'] == 'require-file')
1488
		{
1489 View Code Duplication
			if (!mktree(dirname($action['destination']), 0755) || !is_writable(dirname($action['destination'])))
1490
				$failure |= !mktree(dirname($action['destination']), 0777);
1491
1492
			package_put_contents($action['destination'], package_get_contents($action['source']), $testing_only);
1493
1494
			$failure |= !copy($action['source'], $action['destination']);
1495
1496
			// Any other theme files?
1497
			if (!empty($context['theme_copies']) && !empty($context['theme_copies'][$action['type']][$action['destination']]))
1498
				foreach ($context['theme_copies'][$action['type']][$action['destination']] as $theme_destination)
1499
				{
1500
					if (!mktree(dirname($theme_destination), 0755) || !is_writable(dirname($theme_destination)))
1501
						$failure |= !mktree(dirname($theme_destination), 0777);
1502
1503
					package_put_contents($theme_destination, package_get_contents($action['source']), $testing_only);
1504
1505
					$failure |= !copy($action['source'], $theme_destination);
1506
				}
1507
		}
1508
		elseif ($action['type'] == 'move-file')
1509
		{
1510 View Code Duplication
			if (!mktree(dirname($action['destination']), 0755) || !is_writable(dirname($action['destination'])))
1511
				$failure |= !mktree(dirname($action['destination']), 0777);
1512
1513
			$failure |= !rename($action['source'], $action['destination']);
1514
		}
1515
		elseif ($action['type'] == 'move-dir')
1516
		{
1517 View Code Duplication
			if (!mktree($action['destination'], 0755) || !is_writable($action['destination']))
1518
				$failure |= !mktree($action['destination'], 0777);
1519
1520
			$failure |= !rename($action['source'], $action['destination']);
1521
		}
1522
		elseif ($action['type'] == 'remove-dir')
1523
		{
1524
			deltree($action['filename']);
1525
1526
			// Any other theme folders?
1527
			if (!empty($context['theme_copies']) && !empty($context['theme_copies'][$action['type']][$action['filename']]))
1528
				foreach ($context['theme_copies'][$action['type']][$action['filename']] as $theme_destination)
1529
					deltree($theme_destination);
1530
		}
1531
		elseif ($action['type'] == 'remove-file')
1532
		{
1533
			// Make sure the file exists before deleting it.
1534
			if (file_exists($action['filename']))
1535
			{
1536
				package_chmod($action['filename']);
1537
				$failure |= !unlink($action['filename']);
1538
			}
1539
			// The file that was supposed to be deleted couldn't be found.
1540
			else
1541
				$failure = true;
1542
1543
			// Any other theme folders?
1544
			if (!empty($context['theme_copies']) && !empty($context['theme_copies'][$action['type']][$action['filename']]))
1545
				foreach ($context['theme_copies'][$action['type']][$action['filename']] as $theme_destination)
1546
					if (file_exists($theme_destination))
1547
						$failure |= !unlink($theme_destination);
1548
					else
1549
						$failure = true;
1550
		}
1551
	}
1552
1553
	return $not_done;
1554
}
1555
1556
/**
1557
 * Checks if version matches any of the versions in versions.
1558
 * - supports comma separated version numbers, with or without whitespace.
1559
 * - supports lower and upper bounds. (1.0-1.2)
1560
 * - returns true if the version matched.
1561
 *
1562
 * @param string $versions The SMF versions
1563
 * @param boolean $reset Whether to reset $near_version
1564
 * @param string $the_version
1565
 * @return string|bool Highest install value string or false
1566
 */
1567
function matchHighestPackageVersion($versions, $reset = false, $the_version)
0 ignored issues
show
Coding Style introduced by
Parameters which have default values should be placed at the end.

If you place a parameter with a default value before a parameter with a default value, the default value of the first parameter will never be used as it will always need to be passed anyway:

// $a must always be passed; it's default value is never used.
function someFunction($a = 5, $b) { }
Loading history...
1568
{
1569
	static $near_version = 0;
1570
1571
	if ($reset)
1572
		$near_version = 0;
1573
1574
	// Normalize the $versions while we remove our previous Doh!
1575
	$versions = explode(',', str_replace(array(' ', '2.0rc1-1'), array('', '2.0rc1.1'), strtolower($versions)));
1576
1577
	// Loop through each version, save the highest we can find
1578
	foreach ($versions as $for)
1579
	{
1580
		// Adjust for those wild cards
1581 View Code Duplication
		if (strpos($for, '*') !== false)
1582
			$for = str_replace('*', '0dev0', $for) . '-' . str_replace('*', '999', $for);
1583
1584
		// If we have a range, grab the lower value, done this way so it looks normal-er to the user e.g. 2.0 vs 2.0.99
1585
		if (strpos($for, '-') !== false)
1586
			list ($for, $higher) = explode('-', $for);
0 ignored issues
show
Unused Code introduced by
The assignment to $higher is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1587
1588
		// Do the compare, if the for is greater, than what we have but not greater than what we are running .....
1589
		if (compareVersions($near_version, $for) === -1 && compareVersions($for, $the_version) !== 1)
1590
			$near_version = $for;
1591
	}
1592
1593
	return !empty($near_version) ? $near_version : false;
1594
}
1595
1596
/**
1597
 * Checks if the forum version matches any of the available versions from the package install xml.
1598
 * - supports comma separated version numbers, with or without whitespace.
1599
 * - supports lower and upper bounds. (1.0-1.2)
1600
 * - returns true if the version matched.
1601
 *
1602
 * @param string $version The forum version
1603
 * @param string $versions The versions that this package will install on
1604
 * @return bool Whether the version matched
1605
 */
1606
function matchPackageVersion($version, $versions)
1607
{
1608
	// Make sure everything is lowercase and clean of spaces and unpleasant history.
1609
	$version = str_replace(array(' ', '2.0rc1-1'), array('', '2.0rc1.1'), strtolower($version));
1610
	$versions = explode(',', str_replace(array(' ', '2.0rc1-1'), array('', '2.0rc1.1'), strtolower($versions)));
1611
1612
	// Perhaps we do accept anything?
1613
	if (in_array('all', $versions))
1614
		return true;
1615
1616
	// Loop through each version.
1617
	foreach ($versions as $for)
1618
	{
1619
		// Wild card spotted?
1620 View Code Duplication
		if (strpos($for, '*') !== false)
1621
			$for = str_replace('*', '0dev0', $for) . '-' . str_replace('*', '999', $for);
1622
1623
		// Do we have a range?
1624
		if (strpos($for, '-') !== false)
1625
		{
1626
			list ($lower, $upper) = explode('-', $for);
1627
1628
			// Compare the version against lower and upper bounds.
1629
			if (compareVersions($version, $lower) > -1 && compareVersions($version, $upper) < 1)
1630
				return true;
1631
		}
1632
		// Otherwise check if they are equal...
1633
		elseif (compareVersions($version, $for) === 0)
1634
			return true;
1635
	}
1636
1637
	return false;
1638
}
1639
1640
/**
1641
 * Compares two versions and determines if one is newer, older or the same, returns
1642
 * - (-1) if version1 is lower than version2
1643
 * - (0) if version1 is equal to version2
1644
 * - (1) if version1 is higher than version2
1645
 *
1646
 * @param string $version1 The first version
1647
 * @param string $version2 The second version
1648
 * @return int -1 if version2 is greater than version1, 0 if they're equal, 1 if version1 is greater than version2
1649
 */
1650
function compareVersions($version1, $version2)
1651
{
1652
	static $categories;
1653
1654
	$versions = array();
1655
	foreach (array(1 => $version1, $version2) as $id => $version)
1656
	{
1657
		// Clean the version and extract the version parts.
1658
		$clean = str_replace(array(' ', '2.0rc1-1'), array('', '2.0rc1.1'), strtolower($version));
1659
		preg_match('~(\d+)(?:\.(\d+|))?(?:\.)?(\d+|)(?:(alpha|beta|rc)(\d+|)(?:\.)?(\d+|))?(?:(dev))?(\d+|)~', $clean, $parts);
1660
1661
		// Build an array of parts.
1662
		$versions[$id] = array(
1663
			'major' => !empty($parts[1]) ? (int) $parts[1] : 0,
1664
			'minor' => !empty($parts[2]) ? (int) $parts[2] : 0,
1665
			'patch' => !empty($parts[3]) ? (int) $parts[3] : 0,
1666
			'type' => empty($parts[4]) ? 'stable' : $parts[4],
1667
			'type_major' => !empty($parts[6]) ? (int) $parts[5] : 0,
1668
			'type_minor' => !empty($parts[6]) ? (int) $parts[6] : 0,
1669
			'dev' => !empty($parts[7]),
1670
		);
1671
	}
1672
1673
	// Are they the same, perhaps?
1674
	if ($versions[1] === $versions[2])
1675
		return 0;
1676
1677
	// Get version numbering categories...
1678
	if (!isset($categories))
1679
		$categories = array_keys($versions[1]);
1680
1681
	// Loop through each category.
1682
	foreach ($categories as $category)
1683
	{
1684
		// Is there something for us to calculate?
1685
		if ($versions[1][$category] !== $versions[2][$category])
1686
		{
1687
			// Dev builds are a problematic exception.
1688
			// (stable) dev < (stable) but (unstable) dev = (unstable)
1689
			if ($category == 'type')
1690
				return $versions[1][$category] > $versions[2][$category] ? ($versions[1]['dev'] ? -1 : 1) : ($versions[2]['dev'] ? 1 : -1);
1691
			elseif ($category == 'dev')
1692
				return $versions[1]['dev'] ? ($versions[2]['type'] == 'stable' ? -1 : 0) : ($versions[1]['type'] == 'stable' ? 1 : 0);
1693
			// Otherwise a simple comparison.
1694
			else
1695
				return $versions[1][$category] > $versions[2][$category] ? 1 : -1;
1696
		}
1697
	}
1698
1699
	// They are the same!
1700
	return 0;
1701
}
1702
1703
/**
1704
 * Parses special identifiers out of the specified path.
1705
 *
1706
 * @param string $path The path
1707
 * @return string The parsed path
1708
 */
1709
function parse_path($path)
1710
{
1711
	global $modSettings, $boarddir, $sourcedir, $settings, $temp_path;
1712
1713
	$dirs = array(
1714
		'\\' => '/',
1715
		'$boarddir' => $boarddir,
1716
		'$sourcedir' => $sourcedir,
1717
		'$avatardir' => $modSettings['avatar_directory'],
1718
		'$avatars_dir' => $modSettings['avatar_directory'],
1719
		'$themedir' => $settings['default_theme_dir'],
1720
		'$imagesdir' => $settings['default_theme_dir'] . '/' . basename($settings['default_images_url']),
1721
		'$themes_dir' => $boarddir . '/Themes',
1722
		'$languagedir' => $settings['default_theme_dir'] . '/languages',
1723
		'$languages_dir' => $settings['default_theme_dir'] . '/languages',
1724
		'$smileysdir' => $modSettings['smileys_dir'],
1725
		'$smileys_dir' => $modSettings['smileys_dir'],
1726
	);
1727
1728
	// do we parse in a package directory?
1729
	if (!empty($temp_path))
1730
		$dirs['$package'] = $temp_path;
1731
1732
	if (strlen($path) == 0)
1733
		trigger_error('parse_path(): There should never be an empty filename', E_USER_ERROR);
1734
1735
	return strtr($path, $dirs);
1736
}
1737
1738
/**
1739
 * Deletes a directory, and all the files and direcories inside it.
1740
 * requires access to delete these files.
1741
 *
1742
 * @param string $dir A directory
1743
 * @param bool $delete_dir If false, only deletes everything inside the directory but not the directory itself
1744
 */
1745
function deltree($dir, $delete_dir = true)
1746
{
1747
	/** @var ftp_connection $package_ftp */
1748
	global $package_ftp;
1749
1750
	if (!file_exists($dir))
1751
		return;
1752
1753
	$current_dir = @opendir($dir);
1754
	if ($current_dir == false)
1755
	{
1756
		if ($delete_dir && isset($package_ftp))
1757
		{
1758
			$ftp_file = strtr($dir, array($_SESSION['pack_ftp']['root'] => ''));
1759
			if (!is_dir($dir))
1760
				$package_ftp->chmod($ftp_file, 0777);
1761
			$package_ftp->unlink($ftp_file);
1762
		}
1763
1764
		return;
1765
	}
1766
1767
	while ($entryname = readdir($current_dir))
1768
	{
1769
		if (in_array($entryname, array('.', '..')))
1770
			continue;
1771
1772
		if (is_dir($dir . '/' . $entryname))
1773
			deltree($dir . '/' . $entryname);
1774
		else
1775
		{
1776
			// Here, 755 doesn't really matter since we're deleting it anyway.
1777
			if (isset($package_ftp))
1778
			{
1779
				$ftp_file = strtr($dir . '/' . $entryname, array($_SESSION['pack_ftp']['root'] => ''));
1780
1781
				if (!is_writable($dir . '/' . $entryname))
1782
					$package_ftp->chmod($ftp_file, 0777);
1783
				$package_ftp->unlink($ftp_file);
1784
			}
1785
			else
1786
			{
1787
				if (!is_writable($dir . '/' . $entryname))
1788
					smf_chmod($dir . '/' . $entryname, 0777);
1789
				unlink($dir . '/' . $entryname);
1790
			}
1791
		}
1792
	}
1793
1794
	closedir($current_dir);
1795
1796
	if ($delete_dir)
1797
	{
1798
		if (isset($package_ftp))
1799
		{
1800
			$ftp_file = strtr($dir, array($_SESSION['pack_ftp']['root'] => ''));
1801
			if (!is_writable($dir . '/' . $entryname))
1802
				$package_ftp->chmod($ftp_file, 0777);
1803
			$package_ftp->unlink($ftp_file);
1804
		}
1805
		else
1806
		{
1807
			if (!is_writable($dir))
1808
				smf_chmod($dir, 0777);
1809
			@rmdir($dir);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1810
		}
1811
	}
1812
}
1813
1814
/**
1815
 * Creates the specified tree structure with the mode specified.
1816
 * creates every directory in path until it finds one that already exists.
1817
 *
1818
 * @param string $strPath The path
1819
 * @param int $mode The permission mode for CHMOD (0666, etc.)
1820
 * @return bool True if successful, false otherwise
1821
 */
1822
function mktree($strPath, $mode)
1823
{
1824
	/** @var ftp_connection $package_ftp */
1825
	global $package_ftp;
1826
1827
	if (is_dir($strPath))
1828
	{
1829
		if (!is_writable($strPath) && $mode !== false)
1830
		{
1831 View Code Duplication
			if (isset($package_ftp))
1832
				$package_ftp->chmod(strtr($strPath, array($_SESSION['pack_ftp']['root'] => '')), $mode);
1833
			else
1834
				smf_chmod($strPath, $mode);
1835
		}
1836
1837
		$test = @opendir($strPath);
1838
		if ($test)
1839
		{
1840
			closedir($test);
1841
			return is_writable($strPath);
1842
		}
1843
		else
1844
			return false;
1845
	}
1846
	// Is this an invalid path and/or we can't make the directory?
1847
	if ($strPath == dirname($strPath) || !mktree(dirname($strPath), $mode))
1848
		return false;
1849
1850
	if (!is_writable(dirname($strPath)) && $mode !== false)
1851
	{
1852 View Code Duplication
		if (isset($package_ftp))
1853
			$package_ftp->chmod(dirname(strtr($strPath, array($_SESSION['pack_ftp']['root'] => ''))), $mode);
1854
		else
1855
			smf_chmod(dirname($strPath), $mode);
1856
	}
1857
1858
	if ($mode !== false && isset($package_ftp))
1859
		return $package_ftp->create_dir(strtr($strPath, array($_SESSION['pack_ftp']['root'] => '')));
1860 View Code Duplication
	elseif ($mode === false)
1861
	{
1862
		$test = @opendir(dirname($strPath));
1863
		if ($test)
1864
		{
1865
			closedir($test);
1866
			return true;
1867
		}
1868
		else
1869
			return false;
1870
	}
1871 View Code Duplication
	else
1872
	{
1873
		@mkdir($strPath, $mode);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1874
		$test = @opendir($strPath);
1875
		if ($test)
1876
		{
1877
			closedir($test);
1878
			return true;
1879
		}
1880
		else
1881
			return false;
1882
	}
1883
}
1884
1885
/**
1886
 * Copies one directory structure over to another.
1887
 * requires the destination to be writable.
1888
 *
1889
 * @param string $source The directory to copy
1890
 * @param string $destination The directory to copy $source to
1891
 */
1892
function copytree($source, $destination)
1893
{
1894
	/** @var ftp_connection $package_ftp */
1895
	global $package_ftp;
1896
1897
	if (!file_exists($destination) || !is_writable($destination))
1898
		mktree($destination, 0755);
1899
	if (!is_writable($destination))
1900
		mktree($destination, 0777);
1901
1902
	$current_dir = opendir($source);
1903
	if ($current_dir == false)
1904
		return;
1905
1906
	while ($entryname = readdir($current_dir))
1907
	{
1908
		if (in_array($entryname, array('.', '..')))
1909
			continue;
1910
1911 View Code Duplication
		if (isset($package_ftp))
1912
			$ftp_file = strtr($destination . '/' . $entryname, array($_SESSION['pack_ftp']['root'] => ''));
1913
1914
		if (is_file($source . '/' . $entryname))
1915
		{
1916
			if (isset($package_ftp) && !file_exists($destination . '/' . $entryname))
1917
				$package_ftp->create_file($ftp_file);
0 ignored issues
show
Bug introduced by
The variable $ftp_file does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1918
			elseif (!file_exists($destination . '/' . $entryname))
1919
				@touch($destination . '/' . $entryname);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1920
		}
1921
1922
		package_chmod($destination . '/' . $entryname);
1923
1924
		if (is_dir($source . '/' . $entryname))
1925
			copytree($source . '/' . $entryname, $destination . '/' . $entryname);
1926
		elseif (file_exists($destination . '/' . $entryname))
1927
			package_put_contents($destination . '/' . $entryname, package_get_contents($source . '/' . $entryname));
1928
		else
1929
			copy($source . '/' . $entryname, $destination . '/' . $entryname);
1930
	}
1931
1932
	closedir($current_dir);
1933
}
1934
1935
/**
1936
 * Create a tree listing for a given directory path
1937
 *
1938
 * @param string $path The path
1939
 * @param string $sub_path The sub-path
1940
 * @return array An array of information about the files at the specified path/subpath
1941
 */
1942
function listtree($path, $sub_path = '')
1943
{
1944
	$data = array();
1945
1946
	$dir = @dir($path . $sub_path);
1947
	if (!$dir)
1948
		return array();
1949
	while ($entry = $dir->read())
1950
	{
1951
		if ($entry == '.' || $entry == '..')
1952
			continue;
1953
1954
		if (is_dir($path . $sub_path . '/' . $entry))
1955
			$data = array_merge($data, listtree($path, $sub_path . '/' . $entry));
1956
		else
1957
			$data[] = array(
1958
				'filename' => $sub_path == '' ? $entry : $sub_path . '/' . $entry,
1959
				'size' => filesize($path . $sub_path . '/' . $entry),
1960
				'skipped' => false,
1961
			);
1962
	}
1963
	$dir->close();
1964
1965
	return $data;
1966
}
1967
1968
/**
1969
 * Parses a xml-style modification file (file).
1970
 *
1971
 * @param string $file The modification file to parse
1972
 * @param bool $testing Whether we're just doing a test
1973
 * @param bool $undo If true, specifies that the modifications should be undone. Used when uninstalling. Doesn't work with regex.
1974
 * @param array $theme_paths An array of information about custom themes to apply the changes to
1975
 * @return array An array of those changes made.
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array[].

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
1976
 */
1977
function parseModification($file, $testing = true, $undo = false, $theme_paths = array())
1978
{
1979
	global $boarddir, $sourcedir, $txt, $modSettings;
1980
1981
	@set_time_limit(600);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1982
	require_once($sourcedir . '/Class-Package.php');
1983
	$xml = new xmlArray(strtr($file, array("\r" => '')));
1984
	$actions = array();
1985
	$everything_found = true;
1986
1987
	if (!$xml->exists('modification') || !$xml->exists('modification/file'))
1988
	{
1989
		$actions[] = array(
1990
			'type' => 'error',
1991
			'filename' => '-',
1992
			'debug' => $txt['package_modification_malformed']
1993
		);
1994
		return $actions;
1995
	}
1996
1997
	// Get the XML data.
1998
	$files = $xml->set('modification/file');
1999
2000
	// Use this for holding all the template changes in this mod.
2001
	$template_changes = array();
2002
	// This is needed to hold the long paths, as they can vary...
2003
	$long_changes = array();
2004
2005
	// First, we need to build the list of all the files likely to get changed.
2006
	foreach ($files as $file)
2007
	{
2008
		// What is the filename we're currently on?
2009
		$filename = parse_path(trim($file->fetch('@name')));
2010
2011
		// Now, we need to work out whether this is even a template file...
2012
		foreach ($theme_paths as $id => $theme)
2013
		{
2014
			// If this filename is relative, if so take a guess at what it should be.
2015
			$real_filename = $filename;
2016
			if (strpos($filename, 'Themes') === 0)
2017
				$real_filename = $boarddir . '/' . $filename;
2018
2019 View Code Duplication
			if (strpos($real_filename, $theme['theme_dir']) === 0)
2020
			{
2021
				$template_changes[$id][] = substr($real_filename, strlen($theme['theme_dir']) + 1);
2022
				$long_changes[$id][] = $filename;
2023
			}
2024
		}
2025
	}
2026
2027
	// Custom themes to add.
2028
	$custom_themes_add = array();
2029
2030
	// If we have some template changes, we need to build a master link of what new ones are required for the custom themes.
2031
	if (!empty($template_changes[1]))
2032
	{
2033
		foreach ($theme_paths as $id => $theme)
2034
		{
2035
			// Default is getting done anyway, so no need for involvement here.
2036
			if ($id == 1)
2037
				continue;
2038
2039
			// For every template, do we want it? Yea, no, maybe?
2040
			foreach ($template_changes[1] as $index => $template_file)
2041
			{
2042
				// What, it exists and we haven't already got it?! Lordy, get it in!
2043
				if (file_exists($theme['theme_dir'] . '/' . $template_file) && (!isset($template_changes[$id]) || !in_array($template_file, $template_changes[$id])))
2044
				{
2045
					// Now let's add it to the "todo" list.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
2046
					$custom_themes_add[$long_changes[1][$index]][$id] = $theme['theme_dir'] . '/' . $template_file;
2047
				}
2048
			}
2049
		}
2050
	}
2051
2052
	foreach ($files as $file)
2053
	{
2054
		// This is the actual file referred to in the XML document...
2055
		$files_to_change = array(
2056
			1 => parse_path(trim($file->fetch('@name'))),
2057
		);
2058
2059
		// Sometimes though, we have some additional files for other themes, if we have add them to the mix.
2060
		if (isset($custom_themes_add[$files_to_change[1]]))
2061
			$files_to_change += $custom_themes_add[$files_to_change[1]];
2062
2063
		// Now, loop through all the files we're changing, and, well, change them ;)
2064
		foreach ($files_to_change as $theme => $working_file)
2065
		{
2066 View Code Duplication
			if ($working_file[0] != '/' && $working_file[1] != ':')
2067
			{
2068
				trigger_error('parseModification(): The filename \'' . $working_file . '\' is not a full path!', E_USER_WARNING);
2069
2070
				$working_file = $boarddir . '/' . $working_file;
2071
			}
2072
2073
			// Doesn't exist - give an error or what?
2074
			if (!file_exists($working_file) && (!$file->exists('@error') || !in_array(trim($file->fetch('@error')), array('ignore', 'skip'))))
2075
			{
2076
				$actions[] = array(
2077
					'type' => 'missing',
2078
					'filename' => $working_file,
2079
					'debug' => $txt['package_modification_missing']
2080
				);
2081
2082
				$everything_found = false;
2083
				continue;
2084
			}
2085
			// Skip the file if it doesn't exist.
2086
			elseif (!file_exists($working_file) && $file->exists('@error') && trim($file->fetch('@error')) == 'skip')
2087
			{
2088
				$actions[] = array(
2089
					'type' => 'skipping',
2090
					'filename' => $working_file,
2091
				);
2092
				continue;
2093
			}
2094
			// Okay, we're creating this file then...?
2095
			elseif (!file_exists($working_file))
2096
				$working_data = '';
2097
			// Phew, it exists!  Load 'er up!
2098
			else
2099
				$working_data = str_replace("\r", '', package_get_contents($working_file));
2100
2101
			$actions[] = array(
2102
				'type' => 'opened',
2103
				'filename' => $working_file
2104
			);
2105
2106
			$operations = $file->exists('operation') ? $file->set('operation') : array();
2107
			foreach ($operations as $operation)
2108
			{
2109
				// Convert operation to an array.
2110
				$actual_operation = array(
2111
					'searches' => array(),
2112
					'error' => $operation->exists('@error') && in_array(trim($operation->fetch('@error')), array('ignore', 'fatal', 'required')) ? trim($operation->fetch('@error')) : 'fatal',
2113
				);
2114
2115
				// The 'add' parameter is used for all searches in this operation.
2116
				$add = $operation->exists('add') ? $operation->fetch('add') : '';
2117
2118
				// Grab all search items of this operation (in most cases just 1).
2119
				$searches = $operation->set('search');
2120
				foreach ($searches as $i => $search)
2121
					$actual_operation['searches'][] = array(
2122
						'position' => $search->exists('@position') && in_array(trim($search->fetch('@position')), array('before', 'after', 'replace', 'end')) ? trim($search->fetch('@position')) : 'replace',
2123
						'is_reg_exp' => $search->exists('@regexp') && trim($search->fetch('@regexp')) === 'true',
2124
						'loose_whitespace' => $search->exists('@whitespace') && trim($search->fetch('@whitespace')) === 'loose',
2125
						'search' => $search->fetch('.'),
2126
						'add' => $add,
2127
						'preg_search' => '',
2128
						'preg_replace' => '',
2129
					);
2130
2131
				// At least one search should be defined.
2132 View Code Duplication
				if (empty($actual_operation['searches']))
2133
				{
2134
					$actions[] = array(
2135
						'type' => 'failure',
2136
						'filename' => $working_file,
2137
						'search' => $search['search'],
0 ignored issues
show
Bug introduced by
The variable $search does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2138
						'is_custom' => $theme > 1 ? $theme : 0,
2139
					);
2140
2141
					// Skip to the next operation.
2142
					continue;
2143
				}
2144
2145
				// Reverse the operations in case of undoing stuff.
2146
				if ($undo)
2147
				{
2148
					foreach ($actual_operation['searches'] as $i => $search)
2149
					{
2150
2151
						// Reverse modification of regular expressions are not allowed.
2152
						if ($search['is_reg_exp'])
2153
						{
2154 View Code Duplication
							if ($actual_operation['error'] === 'fatal')
2155
								$actions[] = array(
2156
									'type' => 'failure',
2157
									'filename' => $working_file,
2158
									'search' => $search['search'],
2159
									'is_custom' => $theme > 1 ? $theme : 0,
2160
								);
2161
2162
							// Continue to the next operation.
2163
							continue 2;
2164
						}
2165
2166
						// The replacement is now the search subject...
2167
						if ($search['position'] === 'replace' || $search['position'] === 'end')
2168
							$actual_operation['searches'][$i]['search'] = $search['add'];
2169
						else
2170
						{
2171
							// Reversing a before/after modification becomes a replacement.
2172
							$actual_operation['searches'][$i]['position'] = 'replace';
2173
2174 View Code Duplication
							if ($search['position'] === 'before')
2175
								$actual_operation['searches'][$i]['search'] .= $search['add'];
2176
							elseif ($search['position'] === 'after')
2177
								$actual_operation['searches'][$i]['search'] = $search['add'] . $search['search'];
2178
						}
2179
2180
						// ...and the search subject is now the replacement.
2181
						$actual_operation['searches'][$i]['add'] = $search['search'];
2182
					}
2183
				}
2184
2185
				// Sort the search list so the replaces come before the add before/after's.
2186
				if (count($actual_operation['searches']) !== 1)
2187
				{
2188
					$replacements = array();
2189
2190
					foreach ($actual_operation['searches'] as $i => $search)
2191
					{
2192
						if ($search['position'] === 'replace')
2193
						{
2194
							$replacements[] = $search;
2195
							unset($actual_operation['searches'][$i]);
2196
						}
2197
					}
2198
					$actual_operation['searches'] = array_merge($replacements, $actual_operation['searches']);
2199
				}
2200
2201
				// Create regular expression replacements from each search.
2202
				foreach ($actual_operation['searches'] as $i => $search)
2203
				{
2204
					// Not much needed if the search subject is already a regexp.
2205
					if ($search['is_reg_exp'])
2206
						$actual_operation['searches'][$i]['preg_search'] = $search['search'];
2207
					else
2208
					{
2209
						// Make the search subject fit into a regular expression.
2210
						$actual_operation['searches'][$i]['preg_search'] = preg_quote($search['search'], '~');
2211
2212
						// Using 'loose', a random amount of tabs and spaces may be used.
2213
						if ($search['loose_whitespace'])
2214
							$actual_operation['searches'][$i]['preg_search'] = preg_replace('~[ \t]+~', '[ \t]+', $actual_operation['searches'][$i]['preg_search']);
2215
					}
2216
2217
					// Shuzzup.  This is done so we can safely use a regular expression. ($0 is bad!!)
2218
					$actual_operation['searches'][$i]['preg_replace'] = strtr($search['add'], array('$' => '[$PACK' . 'AGE1$]', '\\' => '[$PACK' . 'AGE2$]'));
2219
2220
					// Before, so the replacement comes after the search subject :P
2221
					if ($search['position'] === 'before')
2222
					{
2223
						$actual_operation['searches'][$i]['preg_search'] = '(' . $actual_operation['searches'][$i]['preg_search'] . ')';
2224
						$actual_operation['searches'][$i]['preg_replace'] = '$1' . $actual_operation['searches'][$i]['preg_replace'];
2225
					}
2226
2227
					// After, after what?
2228 View Code Duplication
					elseif ($search['position'] === 'after')
2229
					{
2230
						$actual_operation['searches'][$i]['preg_search'] = '(' . $actual_operation['searches'][$i]['preg_search'] . ')';
2231
						$actual_operation['searches'][$i]['preg_replace'] .= '$1';
2232
					}
2233
2234
					// Position the replacement at the end of the file (or just before the closing PHP tags).
2235 View Code Duplication
					elseif ($search['position'] === 'end')
2236
					{
2237
						if ($undo)
2238
						{
2239
							$actual_operation['searches'][$i]['preg_replace'] = '';
2240
						}
2241
						else
2242
						{
2243
							$actual_operation['searches'][$i]['preg_search'] = '(\\n\\?\\>)?$';
2244
							$actual_operation['searches'][$i]['preg_replace'] .= '$1';
2245
						}
2246
					}
2247
2248
					// Testing 1, 2, 3...
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
2249
					$failed = preg_match('~' . $actual_operation['searches'][$i]['preg_search'] . '~s', $working_data) === 0;
2250
2251
					// Nope, search pattern not found.
2252
					if ($failed && $actual_operation['error'] === 'fatal')
2253
					{
2254
						$actions[] = array(
2255
							'type' => 'failure',
2256
							'filename' => $working_file,
2257
							'search' => $actual_operation['searches'][$i]['preg_search'],
2258
							'search_original' => $actual_operation['searches'][$i]['search'],
2259
							'replace_original' => $actual_operation['searches'][$i]['add'],
2260
							'position' => $search['position'],
2261
							'is_custom' => $theme > 1 ? $theme : 0,
2262
							'failed' => $failed,
2263
						);
2264
2265
						$everything_found = false;
2266
						continue;
2267
					}
2268
2269
					// Found, but in this case, that means failure!
2270
					elseif (!$failed && $actual_operation['error'] === 'required')
2271
					{
2272
						$actions[] = array(
2273
							'type' => 'failure',
2274
							'filename' => $working_file,
2275
							'search' => $actual_operation['searches'][$i]['preg_search'],
2276
							'search_original' => $actual_operation['searches'][$i]['search'],
2277
							'replace_original' => $actual_operation['searches'][$i]['add'],
2278
							'position' => $search['position'],
2279
							'is_custom' => $theme > 1 ? $theme : 0,
2280
							'failed' => $failed,
2281
						);
2282
2283
						$everything_found = false;
2284
						continue;
2285
					}
2286
2287
					// Replace it into nothing? That's not an option...unless it's an undoing end.
2288
					if ($search['add'] === '' && ($search['position'] !== 'end' || !$undo))
2289
						continue;
2290
2291
					// Finally, we're doing some replacements.
2292
					$working_data = preg_replace('~' . $actual_operation['searches'][$i]['preg_search'] . '~s', $actual_operation['searches'][$i]['preg_replace'], $working_data, 1);
2293
2294
					$actions[] = array(
2295
						'type' => 'replace',
2296
						'filename' => $working_file,
2297
						'search' => $actual_operation['searches'][$i]['preg_search'],
2298
						'replace' =>  $actual_operation['searches'][$i]['preg_replace'],
2299
						'search_original' => $actual_operation['searches'][$i]['search'],
2300
						'replace_original' => $actual_operation['searches'][$i]['add'],
2301
						'position' => $search['position'],
2302
						'failed' => $failed,
2303
						'ignore_failure' => $failed && $actual_operation['error'] === 'ignore',
2304
						'is_custom' => $theme > 1 ? $theme : 0,
2305
					);
2306
				}
2307
			}
2308
2309
			// Fix any little helper symbols ;).
2310
			$working_data = strtr($working_data, array('[$PACK' . 'AGE1$]' => '$', '[$PACK' . 'AGE2$]' => '\\'));
2311
2312
			package_chmod($working_file);
2313
2314
			if ((file_exists($working_file) && !is_writable($working_file)) || (!file_exists($working_file) && !is_writable(dirname($working_file))))
2315
				$actions[] = array(
2316
					'type' => 'chmod',
2317
					'filename' => $working_file
2318
				);
2319
2320
			if (basename($working_file) == 'Settings_bak.php')
2321
				continue;
2322
2323
			if (!$testing && !empty($modSettings['package_make_backups']) && file_exists($working_file))
2324
			{
2325
				// No, no, not Settings.php!
2326
				if (basename($working_file) == 'Settings.php')
2327
					@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 here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
2328
				else
2329
					@copy($working_file, $working_file . '~');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
2330
			}
2331
2332
			// Always call this, even if in testing, because it won't really be written in testing mode.
2333
			package_put_contents($working_file, $working_data, $testing);
2334
2335
			$actions[] = array(
2336
				'type' => 'saved',
2337
				'filename' => $working_file,
2338
				'is_custom' => $theme > 1 ? $theme : 0,
2339
			);
2340
		}
2341
	}
2342
2343
	$actions[] = array(
2344
		'type' => 'result',
2345
		'status' => $everything_found
2346
	);
2347
2348
	return $actions;
2349
}
2350
2351
/**
2352
 * Parses a boardmod-style (.mod) modification file
2353
 *
2354
 * @param string $file The modification file to parse
2355
 * @param bool $testing Whether we're just doing a test
2356
 * @param bool $undo If true, specifies that the modifications should be undone. Used when uninstalling.
2357
 * @param array $theme_paths An array of information about custom themes to apply the changes to
2358
 * @return array An array of those changes made.
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,string|boolean>[].

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
2359
 */
2360
function parseBoardMod($file, $testing = true, $undo = false, $theme_paths = array())
2361
{
2362
	global $boarddir, $sourcedir, $settings, $modSettings;
2363
2364
	@set_time_limit(600);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
2365
	$file = strtr($file, array("\r" => ''));
2366
2367
	$working_file = null;
2368
	$working_search = null;
2369
	$working_data = '';
2370
	$replace_with = null;
2371
2372
	$actions = array();
2373
	$everything_found = true;
2374
2375
	// This holds all the template changes in the standard mod file.
2376
	$template_changes = array();
2377
	// This is just the temporary file.
2378
	$temp_file = $file;
2379
	// This holds the actual changes on a step counter basis.
2380
	$temp_changes = array();
2381
	$counter = 0;
2382
	$step_counter = 0;
2383
2384
	// Before we do *anything*, let's build a list of what we're editing, as it's going to be used for other theme edits.
2385
	while (preg_match('~<(edit file|file|search|search for|add|add after|replace|add before|add above|above|before)>\n(.*?)\n</\\1>~is', $temp_file, $code_match) != 0)
2386
	{
2387
		$counter++;
2388
2389
		// Get rid of the old stuff.
2390
		$temp_file = substr_replace($temp_file, '', strpos($temp_file, $code_match[0]), strlen($code_match[0]));
2391
2392
		// No interest to us?
2393
		if ($code_match[1] != 'edit file' && $code_match[1] != 'file')
2394
		{
2395
			// It's a step, let's add that to the current steps.
2396
			if (isset($temp_changes[$step_counter]))
2397
				$temp_changes[$step_counter]['changes'][] = $code_match[0];
2398
			continue;
2399
		}
2400
2401
		// We've found a new edit - let's make ourself heard, kind of.
2402
		$step_counter = $counter;
2403
		$temp_changes[$step_counter] = array(
2404
			'title' => $code_match[0],
2405
			'changes' => array(),
2406
		);
2407
2408
		$filename = parse_path($code_match[2]);
2409
2410
		// Now, is this a template file, and if so, which?
2411
		foreach ($theme_paths as $id => $theme)
2412
		{
2413
			// If this filename is relative, if so take a guess at what it should be.
2414
			if (strpos($filename, 'Themes') === 0)
2415
				$filename = $boarddir . '/' . $filename;
2416
2417 View Code Duplication
			if (strpos($filename, $theme['theme_dir']) === 0)
2418
				$template_changes[$id][$counter] = substr($filename, strlen($theme['theme_dir']) + 1);
2419
		}
2420
	}
2421
2422
	// Reference for what theme ID this action belongs to.
2423
	$theme_id_ref = array();
2424
2425
	// Now we know what templates we need to touch, cycle through each theme and work out what we need to edit.
2426
	if (!empty($template_changes[1]))
2427
	{
2428
		foreach ($theme_paths as $id => $theme)
2429
		{
2430
			// Don't do default, it means nothing to me.
2431
			if ($id == 1)
2432
				continue;
2433
2434
			// Now, for each file do we need to edit it?
2435
			foreach ($template_changes[1] as $pos => $template_file)
2436
			{
2437
				// It does? Add it to the list darlin'.
2438
				if (file_exists($theme['theme_dir'] . '/' . $template_file) && (!isset($template_changes[$id][$pos]) || !in_array($template_file, $template_changes[$id][$pos])))
2439
				{
2440
					// Actually add it to the mod file too, so we can see that it will work ;)
2441
					if (!empty($temp_changes[$pos]['changes']))
2442
					{
2443
						$file .= "\n\n" . '<edit file>' . "\n" . $theme['theme_dir'] . '/' . $template_file . "\n" . '</edit file>' . "\n\n" . implode("\n\n", $temp_changes[$pos]['changes']);
2444
						$theme_id_ref[$counter] = $id;
2445
						$counter += 1 + count($temp_changes[$pos]['changes']);
2446
					}
2447
				}
2448
			}
2449
		}
2450
	}
2451
2452
	$counter = 0;
2453
	$is_custom = 0;
2454
	while (preg_match('~<(edit file|file|search|search for|add|add after|replace|add before|add above|above|before)>\n(.*?)\n</\\1>~is', $file, $code_match) != 0)
2455
	{
2456
		// This is for working out what we should be editing.
2457
		$counter++;
2458
2459
		// Edit a specific file.
2460
		if ($code_match[1] == 'file' || $code_match[1] == 'edit file')
2461
		{
2462
			// Backup the old file.
2463 View Code Duplication
			if ($working_file !== null)
2464
			{
2465
				package_chmod($working_file);
2466
2467
				// Don't even dare.
2468
				if (basename($working_file) == 'Settings_bak.php')
2469
					continue;
2470
2471
				if (!is_writable($working_file))
2472
					$actions[] = array(
2473
						'type' => 'chmod',
2474
						'filename' => $working_file
2475
					);
2476
2477
				if (!$testing && !empty($modSettings['package_make_backups']) && file_exists($working_file))
2478
				{
2479
					if (basename($working_file) == 'Settings.php')
2480
						@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 here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
2481
					else
2482
						@copy($working_file, $working_file . '~');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
2483
				}
2484
2485
				package_put_contents($working_file, $working_data, $testing);
2486
			}
2487
2488 View Code Duplication
			if ($working_file !== null)
2489
				$actions[] = array(
2490
					'type' => 'saved',
2491
					'filename' => $working_file,
2492
					'is_custom' => $is_custom,
2493
				);
2494
2495
			// Is this "now working on" file a theme specific one?
2496
			$is_custom = isset($theme_id_ref[$counter - 1]) ? $theme_id_ref[$counter - 1] : 0;
2497
2498
			// Make sure the file exists!
2499
			$working_file = parse_path($code_match[2]);
2500
2501 View Code Duplication
			if ($working_file[0] != '/' && $working_file[1] != ':')
2502
			{
2503
				trigger_error('parseBoardMod(): The filename \'' . $working_file . '\' is not a full path!', E_USER_WARNING);
2504
2505
				$working_file = $boarddir . '/' . $working_file;
2506
			}
2507
2508
			if (!file_exists($working_file))
2509
			{
2510
				$places_to_check = array($boarddir, $sourcedir, $settings['default_theme_dir'], $settings['default_theme_dir'] . '/languages');
2511
2512
				foreach ($places_to_check as $place)
2513
					if (file_exists($place . '/' . $working_file))
2514
					{
2515
						$working_file = $place . '/' . $working_file;
2516
						break;
2517
					}
2518
			}
2519
2520
			if (file_exists($working_file))
2521
			{
2522
				// Load the new file.
2523
				$working_data = str_replace("\r", '', package_get_contents($working_file));
2524
2525
				$actions[] = array(
2526
					'type' => 'opened',
2527
					'filename' => $working_file
2528
				);
2529
			}
2530 View Code Duplication
			else
2531
			{
2532
				$actions[] = array(
2533
					'type' => 'missing',
2534
					'filename' => $working_file
2535
				);
2536
2537
				$working_file = null;
2538
				$everything_found = false;
2539
			}
2540
2541
			// Can't be searching for something...
2542
			$working_search = null;
2543
		}
2544
		// Search for a specific string.
2545
		elseif (($code_match[1] == 'search' || $code_match[1] == 'search for') && $working_file !== null)
2546
		{
2547 View Code Duplication
			if ($working_search !== null)
2548
			{
2549
				$actions[] = array(
2550
					'type' => 'error',
2551
					'filename' => $working_file
2552
				);
2553
2554
				$everything_found = false;
2555
			}
2556
2557
			$working_search = $code_match[2];
2558
		}
2559
		// Must've already loaded a search string.
2560
		elseif ($working_search !== null)
2561
		{
2562
			// This is the base string....
2563
			$replace_with = $code_match[2];
2564
2565
			// Add this afterward...
2566
			if ($code_match[1] == 'add' || $code_match[1] == 'add after')
2567
				$replace_with = $working_search . "\n" . $replace_with;
2568
			// Add this beforehand.
2569
			elseif ($code_match[1] == 'before' || $code_match[1] == 'add before' || $code_match[1] == 'above' || $code_match[1] == 'add above')
2570
				$replace_with .= "\n" . $working_search;
2571
			// Otherwise.. replace with $replace_with ;).
2572
		}
2573
2574
		// If we have a search string, replace string, and open file..
2575
		if ($working_search !== null && $replace_with !== null && $working_file !== null)
2576
		{
2577
			// Make sure it's somewhere in the string.
2578
			if ($undo)
2579
			{
2580
				$temp = $replace_with;
2581
				$replace_with = $working_search;
2582
				$working_search = $temp;
2583
			}
2584
2585
			if (strpos($working_data, $working_search) !== false)
2586
			{
2587
				$working_data = str_replace($working_search, $replace_with, $working_data);
2588
2589
				$actions[] = array(
2590
					'type' => 'replace',
2591
					'filename' => $working_file,
2592
					'search' => $working_search,
2593
					'replace' => $replace_with,
2594
					'search_original' => $working_search,
2595
					'replace_original' => $replace_with,
2596
					'position' => $code_match[1] == 'replace' ? 'replace' : ($code_match[1] == 'add' || $code_match[1] == 'add after' ? 'before' : 'after'),
2597
					'is_custom' => $is_custom,
2598
					'failed' => false,
2599
				);
2600
			}
2601
			// It wasn't found!
2602
			else
2603
			{
2604
				$actions[] = array(
2605
					'type' => 'failure',
2606
					'filename' => $working_file,
2607
					'search' => $working_search,
2608
					'is_custom' => $is_custom,
2609
					'search_original' => $working_search,
2610
					'replace_original' => $replace_with,
2611
					'position' => $code_match[1] == 'replace' ? 'replace' : ($code_match[1] == 'add' || $code_match[1] == 'add after' ? 'before' : 'after'),
2612
					'is_custom' => $is_custom,
2613
					'failed' => true,
2614
				);
2615
2616
				$everything_found = false;
2617
			}
2618
2619
			// These don't hold any meaning now.
2620
			$working_search = null;
2621
			$replace_with = null;
2622
		}
2623
2624
		// Get rid of the old tag.
2625
		$file = substr_replace($file, '', strpos($file, $code_match[0]), strlen($code_match[0]));
2626
	}
2627
2628
	// Backup the old file.
2629 View Code Duplication
	if ($working_file !== null)
2630
	{
2631
		package_chmod($working_file);
2632
2633
		if (!is_writable($working_file))
2634
			$actions[] = array(
2635
				'type' => 'chmod',
2636
				'filename' => $working_file
2637
			);
2638
2639
		if (!$testing && !empty($modSettings['package_make_backups']) && file_exists($working_file))
2640
		{
2641
			if (basename($working_file) == 'Settings.php')
2642
				@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 here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
2643
			else
2644
				@copy($working_file, $working_file . '~');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
2645
		}
2646
2647
		package_put_contents($working_file, $working_data, $testing);
2648
	}
2649
2650 View Code Duplication
	if ($working_file !== null)
2651
		$actions[] = array(
2652
			'type' => 'saved',
2653
			'filename' => $working_file,
2654
			'is_custom' => $is_custom,
2655
		);
2656
2657
	$actions[] = array(
2658
		'type' => 'result',
2659
		'status' => $everything_found
2660
	);
2661
2662
	return $actions;
2663
}
2664
2665
/**
2666
 * Get the physical contents of a packages file
2667
 *
2668
 * @param string $filename The package file
2669
 * @return string The contents of the specified file
2670
 */
2671
function package_get_contents($filename)
2672
{
2673
	global $package_cache, $modSettings;
2674
2675 View Code Duplication
	if (!isset($package_cache))
2676
	{
2677
2678
		$mem_check = setMemoryLimit('128M');
2679
2680
		// Windows doesn't seem to care about the memory_limit.
2681
		if (!empty($modSettings['package_disable_cache']) || $mem_check || stripos(PHP_OS, 'win') !== false)
2682
			$package_cache = array();
2683
		else
2684
			$package_cache = false;
2685
	}
2686
2687
	if (strpos($filename, 'Packages/') !== false || $package_cache === false || !isset($package_cache[$filename]))
2688
		return file_get_contents($filename);
2689
	else
2690
		return $package_cache[$filename];
2691
}
2692
2693
/**
2694
 * Writes data to a file, almost exactly like the file_put_contents() function.
2695
 * uses FTP to create/chmod the file when necessary and available.
2696
 * uses text mode for text mode file extensions.
2697
 * returns the number of bytes written.
2698
 *
2699
 * @param string $filename The name of the file
2700
 * @param string $data The data to write to the file
2701
 * @param bool $testing Whether we're just testing things
2702
 * @return int The length of the data written (in bytes)
0 ignored issues
show
Documentation introduced by
Should the return type not be false|integer?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2703
 */
2704
function package_put_contents($filename, $data, $testing = false)
2705
{
2706
	/** @var ftp_connection $package_ftp */
2707
	global $package_ftp, $package_cache, $modSettings;
2708
	static $text_filetypes = array('php', 'txt', '.js', 'css', 'vbs', 'tml', 'htm');
2709
2710 View Code Duplication
	if (!isset($package_cache))
2711
	{
2712
		// Try to increase the memory limit - we don't want to run out of ram!
2713
		$mem_check = setMemoryLimit('128M');
2714
2715
		if (!empty($modSettings['package_disable_cache']) || $mem_check || stripos(PHP_OS, 'win') !== false)
2716
			$package_cache = array();
2717
		else
2718
			$package_cache = false;
2719
	}
2720
2721 View Code Duplication
	if (isset($package_ftp))
2722
		$ftp_file = strtr($filename, array($_SESSION['pack_ftp']['root'] => ''));
2723
2724 View Code Duplication
	if (!file_exists($filename) && isset($package_ftp))
2725
		$package_ftp->create_file($ftp_file);
0 ignored issues
show
Bug introduced by
The variable $ftp_file does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2726
	elseif (!file_exists($filename))
2727
		@touch($filename);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
2728
2729
	package_chmod($filename);
2730
2731
	if (!$testing && (strpos($filename, 'Packages/') !== false || $package_cache === false))
2732
	{
2733
		$fp = @fopen($filename, in_array(substr($filename, -3), $text_filetypes) ? 'w' : 'wb');
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $fp. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
2734
2735
		// We should show an error message or attempt a rollback, no?
2736
		if (!$fp)
2737
			return false;
2738
2739
		fwrite($fp, $data);
2740
		fclose($fp);
2741
	}
2742
	elseif (strpos($filename, 'Packages/') !== false || $package_cache === false)
2743
		return strlen($data);
2744
	else
2745
	{
2746
		$package_cache[$filename] = $data;
2747
2748
		// Permission denied, eh?
2749
		$fp = @fopen($filename, 'r+');
2750
		if (!$fp)
2751
			return false;
2752
		fclose($fp);
2753
	}
2754
2755
	return strlen($data);
2756
}
2757
2758
/**
2759
 * Flushes the cache from memory to the filesystem
2760
 *
2761
 * @param bool $trash
2762
 */
2763
function package_flush_cache($trash = false)
2764
{
2765
	/** @var ftp_connection $package_ftp */
2766
	global $package_ftp, $package_cache;
2767
	static $text_filetypes = array('php', 'txt', '.js', 'css', 'vbs', 'tml', 'htm');
2768
2769
	if (empty($package_cache))
2770
		return;
2771
2772
	// First, let's check permissions!
2773
	foreach ($package_cache as $filename => $data)
2774
	{
2775 View Code Duplication
		if (isset($package_ftp))
2776
			$ftp_file = strtr($filename, array($_SESSION['pack_ftp']['root'] => ''));
2777
2778 View Code Duplication
		if (!file_exists($filename) && isset($package_ftp))
2779
			$package_ftp->create_file($ftp_file);
0 ignored issues
show
Bug introduced by
The variable $ftp_file does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2780
		elseif (!file_exists($filename))
2781
			@touch($filename);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
2782
2783
		$result = package_chmod($filename);
2784
2785
		// if we are not doing our test pass, then lets do a full write check
2786
		// bypass directories when doing this test
2787
		if ((!$trash) && !is_dir($filename))
2788
		{
2789
			// acid test, can we really open this file for writing?
2790
			$fp = ($result) ? fopen($filename, 'r+') : $result;
2791
			if (!$fp)
2792
			{
2793
				// We should have package_chmod()'d them before, no?!
2794
				trigger_error('package_flush_cache(): some files are still not writable', E_USER_WARNING);
2795
				return;
2796
			}
2797
			fclose($fp);
2798
		}
2799
	}
2800
2801
	if ($trash)
2802
	{
2803
		$package_cache = array();
2804
		return;
2805
	}
2806
2807
	// Write the cache to disk here.
2808
	// Bypass directories when doing so - no data to write & the fopen will crash.
2809
	foreach ($package_cache as $filename => $data)
2810
	{
2811
		if (!is_dir($filename))
2812
		{
2813
			$fp = fopen($filename, in_array(substr($filename, -3), $text_filetypes) ? 'w' : 'wb');
2814
			fwrite($fp, $data);
2815
			fclose($fp);
2816
		}
2817
	}
2818
2819
	$package_cache = array();
2820
}
2821
2822
/**
2823
 * Try to make a file writable.
2824
 *
2825
 * @param string $filename The name of the file
2826
 * @param string $perm_state The permission state - can be either 'writable' or 'execute'
2827
 * @param bool $track_change Whether to track this change
2828
 * @return boolean True if it worked, false if it didn't
2829
 */
2830
function package_chmod($filename, $perm_state = 'writable', $track_change = false)
2831
{
2832
	/** @var ftp_connection $package_ftp */
2833
	global $package_ftp;
2834
2835
	if (file_exists($filename) && is_writable($filename) && $perm_state == 'writable')
2836
		return true;
2837
2838
	// Start off checking without FTP.
2839
	if (!isset($package_ftp) || $package_ftp === false)
2840
	{
2841
		for ($i = 0; $i < 2; $i++)
2842
		{
2843
			$chmod_file = $filename;
2844
2845
			// Start off with a less aggressive test.
2846
			if ($i == 0)
2847
			{
2848
				// If this file doesn't exist, then we actually want to look at whatever parent directory does.
2849
				$subTraverseLimit = 2;
2850
				while (!file_exists($chmod_file) && $subTraverseLimit)
2851
				{
2852
					$chmod_file = dirname($chmod_file);
2853
					$subTraverseLimit--;
2854
				}
2855
2856
				// Keep track of the writable status here.
2857
				$file_permissions = @fileperms($chmod_file);
2858
			}
2859
			else
2860
			{
2861
				// This looks odd, but it's an attempt to work around PHP suExec.
2862
				if (!file_exists($chmod_file) && $perm_state == 'writable')
2863
				{
2864
					$file_permissions = @fileperms(dirname($chmod_file));
2865
2866
					mktree(dirname($chmod_file), 0755);
2867
					@touch($chmod_file);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
2868
					smf_chmod($chmod_file, 0755);
2869
				}
2870
				else
2871
					$file_permissions = @fileperms($chmod_file);
2872
			}
2873
2874
			// This looks odd, but it's another attempt to work around PHP suExec.
2875
			if ($perm_state != 'writable')
2876
				smf_chmod($chmod_file, $perm_state == 'execute' ? 0755 : 0644);
2877
			else
2878
			{
2879
				if (!@is_writable($chmod_file))
2880
					smf_chmod($chmod_file, 0755);
2881
				if (!@is_writable($chmod_file))
2882
					smf_chmod($chmod_file, 0777);
2883
				if (!@is_writable(dirname($chmod_file)))
2884
					smf_chmod($chmod_file, 0755);
2885
				if (!@is_writable(dirname($chmod_file)))
2886
					smf_chmod($chmod_file, 0777);
2887
			}
2888
2889
			// The ultimate writable test.
2890
			if ($perm_state == 'writable')
2891
			{
2892
				$fp = is_dir($chmod_file) ? @opendir($chmod_file) : @fopen($chmod_file, 'rb');
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $fp. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
2893
				if (@is_writable($chmod_file) && $fp)
2894
				{
2895
					if (!is_dir($chmod_file))
2896
						fclose($fp);
2897
					else
2898
						closedir($fp);
2899
2900
					// It worked!
2901
					if ($track_change)
2902
						$_SESSION['pack_ftp']['original_perms'][$chmod_file] = $file_permissions;
2903
2904
					return true;
2905
				}
2906
			}
2907 View Code Duplication
			elseif ($perm_state != 'writable' && isset($_SESSION['pack_ftp']['original_perms'][$chmod_file]))
2908
				unset($_SESSION['pack_ftp']['original_perms'][$chmod_file]);
2909
		}
2910
2911
		// If we're here we're a failure.
2912
		return false;
2913
	}
2914
	// Otherwise we do have FTP?
2915
	elseif ($package_ftp !== false && !empty($_SESSION['pack_ftp']))
2916
	{
2917
		$ftp_file = strtr($filename, array($_SESSION['pack_ftp']['root'] => ''));
2918
2919
		// This looks odd, but it's an attempt to work around PHP suExec.
2920
		if (!file_exists($filename) && $perm_state == 'writable')
2921
		{
2922
			$file_permissions = @fileperms(dirname($filename));
2923
2924
			mktree(dirname($filename), 0755);
2925
			$package_ftp->create_file($ftp_file);
2926
			$package_ftp->chmod($ftp_file, 0755);
2927
		}
2928
		else
2929
			$file_permissions = @fileperms($filename);
2930
2931
		if ($perm_state != 'writable')
2932
		{
2933
			$package_ftp->chmod($ftp_file, $perm_state == 'execute' ? 0755 : 0644);
2934
		}
2935
		else
2936
		{
2937
			if (!@is_writable($filename))
2938
				$package_ftp->chmod($ftp_file, 0777);
2939
			if (!@is_writable(dirname($filename)))
2940
				$package_ftp->chmod(dirname($ftp_file), 0777);
2941
		}
2942
2943
		if (@is_writable($filename))
2944
		{
2945
			if ($track_change)
2946
				$_SESSION['pack_ftp']['original_perms'][$filename] = $file_permissions;
2947
2948
			return true;
2949
		}
2950 View Code Duplication
		elseif ($perm_state != 'writable' && isset($_SESSION['pack_ftp']['original_perms'][$filename]))
2951
			unset($_SESSION['pack_ftp']['original_perms'][$filename]);
2952
	}
2953
2954
	// Oh dear, we failed if we get here.
2955
	return false;
2956
}
2957
2958
/**
2959
 * Used to crypt the supplied ftp password in this session
2960
 *
2961
 * @param string $pass The password
2962
 * @return string The encrypted password
2963
 */
2964
function package_crypt($pass)
2965
{
2966
	$n = strlen($pass);
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $n. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
2967
2968
	$salt = session_id();
2969
	while (strlen($salt) < $n)
2970
		$salt .= session_id();
2971
2972
	for ($i = 0; $i < $n; $i++)
2973
		$pass{$i} = chr(ord($pass{$i}) ^ (ord($salt{$i}) - 32));
2974
2975
	return $pass;
2976
}
2977
2978
/**
2979
 * Creates a backup of forum files prior to modifying them
2980
 * @param string $id The name of the backup
2981
 * @return bool True if it worked, false if it didn't
2982
 */
2983
function package_create_backup($id = 'backup')
2984
{
2985
	global $sourcedir, $boarddir, $packagesdir, $smcFunc;
2986
2987
	$files = array();
2988
2989
	$base_files = array('index.php', 'SSI.php', 'agreement.txt', 'cron.php', 'ssi_examples.php', 'ssi_examples.shtml', 'subscriptions.php');
2990
	foreach ($base_files as $file)
2991
	{
2992
		if (file_exists($boarddir . '/' . $file))
2993
			$files[empty($_REQUEST['use_full_paths']) ? $file : $boarddir . '/' . $file] = $boarddir . '/' . $file;
2994
	}
2995
2996
	$dirs = array(
2997
		$sourcedir => empty($_REQUEST['use_full_paths']) ? 'Sources/' : strtr($sourcedir . '/', '\\', '/')
2998
	);
2999
3000
	$request = $smcFunc['db_query']('', '
3001
		SELECT value
3002
		FROM {db_prefix}themes
3003
		WHERE id_member = {int:no_member}
3004
			AND variable = {string:theme_dir}',
3005
		array(
3006
			'no_member' => 0,
3007
			'theme_dir' => 'theme_dir',
3008
		)
3009
	);
3010
	while ($row = $smcFunc['db_fetch_assoc']($request))
3011
		$dirs[$row['value']] = empty($_REQUEST['use_full_paths']) ? 'Themes/' . basename($row['value']) . '/' : strtr($row['value'] . '/', '\\', '/');
3012
	$smcFunc['db_free_result']($request);
3013
3014
	try
3015
	{
3016
		foreach ($dirs as $dir => $dest)
3017
		{
3018
			$iter = new RecursiveIteratorIterator(
3019
				new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
3020
				RecursiveIteratorIterator::CHILD_FIRST,
3021
				RecursiveIteratorIterator::CATCH_GET_CHILD // Ignore "Permission denied"
3022
			);
3023
3024
			foreach ($iter as $entry => $dir)
3025
			{
3026
				if ($dir->isDir())
3027
					continue;
3028
3029
				if (preg_match('~^(\.{1,2}|CVS|backup.*|help|images|.*\~)$~', $entry) != 0)
3030
					continue;
3031
3032
				$files[empty($_REQUEST['use_full_paths']) ? str_replace(realpath($boarddir), '', $entry) : $entry] = $entry;
3033
			}
3034
		}
3035
		$obj = new ArrayObject($files);
3036
		$iterator = $obj->getIterator();
3037
3038
		if (!file_exists($packagesdir . '/backups'))
3039
			mktree($packagesdir . '/backups', 0777);
3040
		if (!is_writable($packagesdir . '/backups'))
3041
			package_chmod($packagesdir . '/backups');
3042
		$output_file = $packagesdir . '/backups/' . strftime('%Y-%m-%d_') . preg_replace('~[$\\\\/:<>|?*"\']~', '', $id);
3043
		$output_ext = '.tar';
3044
		$output_ext_target = '.tar.gz';
3045
3046
		if (file_exists($output_file . $output_ext_target))
3047
		{
3048
			$i = 2;
3049
			while (file_exists($output_file . '_' . $i . $output_ext_target))
3050
				$i++;
3051
			$output_file = $output_file . '_' . $i . $output_ext;
3052
		}
3053
		else
3054
			$output_file .= $output_ext;
3055
3056
		@set_time_limit(300);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
3057
		if (function_exists('apache_reset_timeout'))
3058
			@apache_reset_timeout();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
3059
3060
		$a = new PharData($output_file);
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $a. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
3061
		$a->buildFromIterator($iterator);
3062
		$a->compress(Phar::GZ);
3063
3064
		/*
3065
		 * Destroying the local var tells PharData to close its internal
3066
		 * file pointer, enabling us to delete the uncompressed tarball.
3067
		 */
3068
		unset($a);
3069
		unlink($output_file);
3070
	}
3071
	catch (Exception $e)
3072
	{
3073
		log_error($e->getMessage(), 'backup');
3074
3075
		return false;
3076
	}
3077
3078
	return true;
3079
}
3080
3081
/**
3082
 * Get the contents of a URL, irrespective of allow_url_fopen.
3083
 *
3084
 * - reads the contents of an http or ftp address and retruns the page in a string
3085
 * - will accept up to 3 page redirections (redirectio_level in the function call is private)
3086
 * - if post_data is supplied, the value and length is posted to the given url as form data
3087
 * - URL must be supplied in lowercase
3088
 *
3089
 * @param string $url The URL
3090
 * @param string $post_data The data to post to the given URL
3091
 * @param bool $keep_alive Whether to send keepalive info
3092
 * @param int $redirection_level How many levels of redirection
3093
 * @return string|false The fetched data or false on failure
3094
 */
3095
function fetch_web_data($url, $post_data = '', $keep_alive = false, $redirection_level = 0)
3096
{
3097
	global $webmaster_email, $sourcedir;
3098
	static $keep_alive_dom = null, $keep_alive_fp = null;
3099
3100
	preg_match('~^(http|ftp)(s)?://([^/:]+)(:(\d+))?(.+)$~', $url, $match);
3101
3102
	// An FTP url. We should try connecting and RETRieving it...
3103
	if (empty($match[1]))
3104
		return false;
3105
	elseif ($match[1] == 'ftp')
3106
	{
3107
		// Include the file containing the ftp_connection class.
3108
		require_once($sourcedir . '/Class-Package.php');
3109
3110
		// Establish a connection and attempt to enable passive mode.
3111
		$ftp = new ftp_connection(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? 21 : $match[5], 'anonymous', $webmaster_email);
3112
		if ($ftp->error !== false || !$ftp->passive())
3113
			return false;
3114
3115
		// I want that one *points*!
3116
		fwrite($ftp->connection, 'RETR ' . $match[6] . "\r\n");
3117
3118
		// Since passive mode worked (or we would have returned already!) open the connection.
3119
		$fp = @fsockopen($ftp->pasv['ip'], $ftp->pasv['port'], $err, $err, 5);
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $fp. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
3120
		if (!$fp)
3121
			return false;
3122
3123
		// The server should now say something in acknowledgement.
3124
		$ftp->check_response(150);
3125
3126
		$data = '';
3127
		while (!feof($fp))
3128
			$data .= fread($fp, 4096);
3129
		fclose($fp);
3130
3131
		// All done, right?  Good.
3132
		$ftp->check_response(226);
3133
		$ftp->close();
3134
	}
3135
	// More likely a standard HTTP URL, first try to use cURL if available
3136
	elseif (isset($match[1]) && $match[1] === 'http' && function_exists('curl_init'))
3137
	{
3138
		// Include the file containing the curl_fetch_web_data class.
3139
		require_once($sourcedir . '/Class-CurlFetchWeb.php');
3140
3141
		$fetch_data = new curl_fetch_web_data();
3142
		$fetch_data->get_url_data($url, $post_data);
0 ignored issues
show
Documentation introduced by
$post_data is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
3143
3144
		// no errors and a 200 result, then we have a good dataset, well we at least have data ;)
3145
		if ($fetch_data->result('code') == 200 && !$fetch_data->result('error'))
3146
			$data = $fetch_data->result('body');
3147
		else
3148
			return false;
3149
	}
3150
	// This is more likely; a standard HTTP URL.
3151
	elseif (isset($match[1]) && $match[1] == 'http')
3152
	{
3153
		if ($keep_alive && $match[3] == $keep_alive_dom)
3154
			$fp = $keep_alive_fp;
3155
		if (empty($fp))
3156
		{
3157
			// Open the socket on the port we want...
3158
			$fp = @fsockopen(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? ($match[2] ? 443 : 80) : $match[5], $err, $err, 5);
3159
			if (!$fp)
3160
				return false;
3161
		}
3162
3163
		if ($keep_alive)
3164
		{
3165
			$keep_alive_dom = $match[3];
3166
			$keep_alive_fp = $fp;
3167
		}
3168
3169
		// I want this, from there, and I'm not going to be bothering you for more (probably.)
3170
		if (empty($post_data))
3171
		{
3172
			fwrite($fp, 'GET ' . ($match[6] !== '/' ? str_replace(' ', '%20', $match[6]) : '') . ' HTTP/1.0' . "\r\n");
3173
			fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
3174
			fwrite($fp, 'User-Agent: PHP/SMF' . "\r\n");
3175
			if ($keep_alive)
3176
				fwrite($fp, 'Connection: Keep-Alive' . "\r\n\r\n");
3177
			else
3178
				fwrite($fp, 'Connection: close' . "\r\n\r\n");
3179
		}
3180
		else
3181
		{
3182
			fwrite($fp, 'POST ' . ($match[6] !== '/' ? $match[6] : '') . ' HTTP/1.0' . "\r\n");
3183
			fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
3184
			fwrite($fp, 'User-Agent: PHP/SMF' . "\r\n");
3185
			if ($keep_alive)
3186
				fwrite($fp, 'Connection: Keep-Alive' . "\r\n");
3187
			else
3188
				fwrite($fp, 'Connection: close' . "\r\n");
3189
			fwrite($fp, 'Content-Type: application/x-www-form-urlencoded' . "\r\n");
3190
			fwrite($fp, 'Content-Length: ' . strlen($post_data) . "\r\n\r\n");
3191
			fwrite($fp, $post_data);
3192
		}
3193
3194
		$response = fgets($fp, 768);
3195
3196
		// Redirect in case this location is permanently or temporarily moved.
3197
		if ($redirection_level < 3 && preg_match('~^HTTP/\S+\s+30[127]~i', $response) === 1)
3198
		{
3199
			$header = '';
3200
			$location = '';
3201
			while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
3202
				if (strpos($header, 'Location:') !== false)
3203
					$location = trim(substr($header, strpos($header, ':') + 1));
3204
3205
			if (empty($location))
3206
				return false;
3207
			else
3208
			{
3209
				if (!$keep_alive)
3210
					fclose($fp);
3211
				return fetch_web_data($location, $post_data, $keep_alive, $redirection_level + 1);
3212
			}
3213
		}
3214
3215
		// Make sure we get a 200 OK.
3216
		elseif (preg_match('~^HTTP/\S+\s+20[01]~i', $response) === 0)
3217
			return false;
3218
3219
		// Skip the headers...
3220
		while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
3221
		{
3222
			if (preg_match('~content-length:\s*(\d+)~i', $header, $match) != 0)
3223
				$content_length = $match[1];
3224
			elseif (preg_match('~connection:\s*close~i', $header) != 0)
3225
			{
3226
				$keep_alive_dom = null;
3227
				$keep_alive = false;
3228
			}
3229
3230
			continue;
3231
		}
3232
3233
		$data = '';
3234
		if (isset($content_length))
3235
		{
3236
			while (!feof($fp) && strlen($data) < $content_length)
3237
				$data .= fread($fp, $content_length - strlen($data));
3238
		}
3239
		else
3240
		{
3241
			while (!feof($fp))
3242
				$data .= fread($fp, 4096);
3243
		}
3244
3245
		if (!$keep_alive)
3246
			fclose($fp);
3247
	}
3248
	else
3249
	{
3250
		// Umm, this shouldn't happen?
3251
		trigger_error('fetch_web_data(): Bad URL', E_USER_NOTICE);
3252
		$data = false;
3253
	}
3254
3255
	return $data;
3256
}
3257
3258 View Code Duplication
if (!function_exists('smf_crc32'))
3259
{
3260
	/**
3261
	 * crc32 doesn't work as expected on 64-bit functions - make our own.
3262
	 * https://php.net/crc32#79567
3263
	 *
3264
	 * @param string $number
3265
	 * @return string The crc32
0 ignored issues
show
Documentation introduced by
Should the return type not be integer?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
3266
	 */
3267
	function smf_crc32($number)
0 ignored issues
show
Best Practice introduced by
The function smf_crc32() has been defined more than once; this definition is ignored, only the first definition in Sources/Subs-Compat.php (L164-176) is considered.

This check looks for functions that have already been defined in other files.

Some Codebases, like WordPress, make a practice of defining functions multiple times. This may lead to problems with the detection of function parameters and types. If you really need to do this, you can mark the duplicate definition with the @ignore annotation.

/**
 * @ignore
 */
function getUser() {

}

function getUser($id, $realm) {

}

See also the PhpDoc documentation for @ignore.

Loading history...
3268
	{
3269
		$crc = crc32($number);
3270
3271
		if ($crc & 0x80000000)
3272
		{
3273
			$crc ^= 0xffffffff;
3274
			$crc += 1;
0 ignored issues
show
Coding Style introduced by
Increment operators should be used where possible; found "$crc += 1;" but expected "$crc++"
Loading history...
3275
			$crc = -$crc;
3276
		}
3277
3278
		return $crc;
3279
	}
3280
}
3281
3282
?>