Completed
Push — master ( cc3eab...992b40 )
by Nazar
12:34 queued 01:59
created

get_dependencies_conflicts()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 4

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 11
rs 9.4285
cc 1
eloc 4
nc 1
nop 2
1
<?php
2
/**
3
 * @package    CleverStyle CMS
4
 * @subpackage System module
5
 * @category   modules
6
 * @author     Nazar Mokrynskyi <[email protected]>
7
 * @copyright  Copyright (c) 2015-2016, Nazar Mokrynskyi
8
 * @license    MIT License, see license.txt
9
 */
10
namespace cs\modules\System;
11
use
12
	cs\Config,
13
	cs\Core,
14
	cs\DB,
15
	cs\Language,
16
	cs\Page;
17
18
/**
19
 * Utility functions, necessary during packages manipulation (installation/uninstallation, enabling/disabling)
20
 */
21
class Packages_manipulation {
22
	/**
23
	 * @param string $file_name File key in `$_FILES` superglobal
24
	 *
25
	 * @return false|string Path to file location if succeed or `false` on failure
26
	 */
27
	static function move_uploaded_file_to_tmp ($file_name) {
28
		if (!isset($_FILES[$file_name]) || !$_FILES[$file_name]['tmp_name']) {
29
			return false;
30
		}
31
		$L    = Language::instance();
32
		$Page = Page::instance();
33
		switch ($_FILES[$file_name]['error']) {
34
			case UPLOAD_ERR_INI_SIZE:
35
			case UPLOAD_ERR_FORM_SIZE:
36
				$Page->warning($L->file_too_large);
37
				return false;
38
			case UPLOAD_ERR_NO_TMP_DIR:
39
				$Page->warning($L->temporary_folder_is_missing);
40
				return false;
41
			case UPLOAD_ERR_CANT_WRITE:
42
				$Page->warning($L->cant_write_file_to_disk);
43
				return false;
44
			case UPLOAD_ERR_PARTIAL:
45
			case UPLOAD_ERR_NO_FILE:
46
				return false;
47
		}
48
		if ($_FILES[$file_name]['error'] != UPLOAD_ERR_OK) {
49
			return false;
50
		}
51
		$tmp_name = TEMP.'/'.md5(random_bytes(1000)).'.phar';
52
		return move_uploaded_file($_FILES[$file_name]['tmp_name'], $tmp_name) ? $tmp_name : false;
53
	}
54
	/**
55
	 * Generic extraction of files from phar distributive for CleverStyle CMS (components installation)
56
	 *
57
	 * @param string $target_directory
58
	 * @param string $source_phar Will be removed after extraction
59
	 *
60
	 * @return bool
61
	 */
62
	static function install_extract ($target_directory, $source_phar) {
63
		if (!mkdir($target_directory, 0770)) {
64
			return false;
65
		}
66
		$tmp_dir   = "phar://$source_phar";
67
		$fs        = file_get_json("$tmp_dir/fs.json");
68
		$extracted = array_filter(
69
			array_map(
70
				function ($index, $file) use ($tmp_dir, $target_directory) {
71
					if (
72
						!@mkdir(dirname("$target_directory/$file"), 0770, true) &&
73
						!is_dir(dirname("$target_directory/$file"))
74
					) {
75
						return false;
76
					}
77
					/**
78
					 * TODO: copy() + file_exists() is a hack for HHVM, when bug fixed upstream (copying of empty files) this should be simplified
79
					 */
80
					copy("$tmp_dir/fs/$index", "$target_directory/$file");
81
					return file_exists("$target_directory/$file");
82
				},
83
				$fs,
84
				array_keys($fs)
85
			)
86
		);
87
		unlink($source_phar);
1 ignored issue
show
Security File Manipulation introduced by
$source_phar can contain request data and is used in file manipulation context(s) leading to a potential security vulnerability.

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
88
		if (count($extracted) === count($fs)) {
89
			file_put_json("$target_directory/fs.json", array_keys($fs));
90
			return true;
91
		}
92
		return false;
93
	}
94
	/**
95
	 * Generic extraction of files from phar distributive for CleverStyle CMS (system and components update)
96
	 *
97
	 * @param string      $target_directory
98
	 * @param string      $source_phar             Will be removed after extraction
99
	 * @param null|string $fs_location_directory   Defaults to `$target_directory`
100
	 * @param null|string $meta_location_directory Defaults to `$target_directory`
101
	 *
102
	 * @return bool
103
	 */
104
	static function update_extract ($target_directory, $source_phar, $fs_location_directory = null, $meta_location_directory = null) {
105
		$fs_location_directory   = $fs_location_directory ?: $target_directory;
106
		$meta_location_directory = $meta_location_directory ?: $target_directory;
107
		/**
108
		 * Backup some necessary information about current version
109
		 */
110
		copy("$fs_location_directory/fs.json", "$fs_location_directory/fs_backup.json");
111
		copy("$meta_location_directory/meta.json", "$meta_location_directory/meta_backup.json");
112
		/**
113
		 * Extracting new versions of files
114
		 */
115
		$tmp_dir   = "phar://$source_phar";
116
		$fs        = file_get_json("$tmp_dir/fs.json");
117
		$extracted = array_filter(
118
			array_map(
119
				function ($index, $file) use ($tmp_dir, $target_directory) {
120
					if (
121
						!@mkdir(dirname("$target_directory/$file"), 0770, true) &&
122
						!is_dir(dirname("$target_directory/$file"))
123
					) {
124
						return false;
125
					}
126
					/**
127
					 * TODO: copy() + file_exists() is a hack for HHVM, when bug fixed upstream (copying of empty files) this should be simplified
128
					 */
129
					copy("$tmp_dir/fs/$index", "$target_directory/$file");
130
					return file_exists("$target_directory/$file");
131
				},
132
				$fs,
133
				array_keys($fs)
134
			)
135
		);
136
		unlink($source_phar);
1 ignored issue
show
Security File Manipulation introduced by
$source_phar can contain request data and is used in file manipulation context(s) leading to a potential security vulnerability.

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
137
		unset($tmp_dir);
138
		if (count($extracted) !== count($fs)) {
139
			return false;
140
		}
141
		unset($extract);
142
		$fs = array_keys($fs);
143
		/**
144
		 * Removing of old unnecessary files and directories
145
		 */
146
		foreach (
147
			array_diff(
148
				file_get_json("$fs_location_directory/fs_backup.json"),
149
				$fs
150
			) as $file
151
		) {
152
			$file = "$target_directory/$file";
153
			if (file_exists($file) && is_writable($file)) {
154
				unlink($file);
155
				// Recursively remove all empty parent directories
156
				while (!get_files_list($file = dirname($file))) {
157
					rmdir($file);
158
				}
159
			}
160
		}
161
		unset($file, $dir);
162
		file_put_json("$fs_location_directory/fs.json", $fs);
163
		return true;
164
	}
165
	/**
166
	 * Generic update for CleverStyle CMS (system and components), runs PHP scripts and does DB migrations after extracting of new distributive
167
	 *
168
	 * @param string     $target_directory
169
	 * @param string     $old_version
170
	 * @param array|null $db_array `$Config->components['modules'][$module]['db']` if module or system
171
	 */
172
	static function update_php_sql ($target_directory, $old_version, $db_array = null) {
173
		foreach (self::get_update_versions($target_directory) as $version) {
174
			if (version_compare($old_version, $version, '<')) {
175
				/**
176
				 * PHP update script
177
				 */
178
				_include_once("$target_directory/meta/update/$version.php", false);
179
				/**
180
				 * Database update
181
				 */
182
				if ($db_array) {
183
					self::execute_sql_from_directory("$target_directory/meta/update_db", $db_array, $version);
184
				}
185
			}
186
		}
187
	}
188
	/**
189
	 * @param string $target_directory
190
	 *
191
	 * @return string[]
192
	 */
193
	protected static function get_update_versions ($target_directory) {
194
		$update_versions = _mb_substr(get_files_list("$target_directory/meta/update"), 0, -4) ?: [];
195
		foreach (get_files_list("$target_directory/meta/update_db", false, 'd') ?: [] as $db) {
196
			/** @noinspection SlowArrayOperationsInLoopInspection */
197
			$update_versions = array_merge(
198
				$update_versions,
199
				get_files_list("$target_directory/meta/update_db/$db", false, 'd') ?: []
200
			);
201
		}
202
		$update_versions = array_unique($update_versions);
203
		usort($update_versions, 'version_compare');
204
		return $update_versions;
205
	}
206
	/**
207
	 * @param string $directory        Base path to SQL files
208
	 * @param array  $db_configuration Array in form [$db_name => $index]
209
	 * @param string $version          In case when we are working with update script we might have version subdirectory
210
	 */
211
	static function execute_sql_from_directory ($directory, $db_configuration, $version = '') {
212
		$Config = Config::instance();
213
		$Core   = Core::instance();
214
		$db     = DB::instance();
215
		time_limit_pause();
216
		foreach ($db_configuration as $db_name => $index) {
217
			$db_type  = $index == 0 ? $Core->db_type : $Config->db[$index]['type'];
218
			$sql_file = "$directory/$db_name/$version/$db_type.sql";
219
			if (file_exists($sql_file)) {
220
				$db->db_prime($index)->q(
221
					explode(';', file_get_contents($sql_file))
222
				);
223
			}
224
		}
225
		time_limit_pause(false);
226
	}
227
	/**
228
	 * Check dependencies for new component (during installation/updating/enabling)
229
	 *
230
	 * @param array $meta `meta.json` contents of target component
231
	 *
232
	 * @return array
233
	 */
234
	static function get_dependencies ($meta) {
235
		/**
236
		 * No `meta.json` - nothing to check, allow it
237
		 */
238
		if (!$meta) {
239
			return [];
240
		}
241
		$meta         = self::normalize_meta($meta);
242
		$Config       = Config::instance();
243
		$dependencies = [];
244
		/**
245
		 * Check for compatibility with modules
246
		 */
247
		foreach (array_keys($Config->components['modules']) as $module) {
248
			/**
249
			 * If module uninstalled - we do not care about it
250
			 */
251
			if ($Config->module($module)->uninstalled()) {
252
				continue;
253
			}
254
			/**
255
			 * Stub for the case if there is no `meta.json`
256
			 */
257
			$module_meta = [
258
				'package'  => $module,
259
				'category' => 'modules',
260
				'version'  => 0
261
			];
262
			if (file_exists(MODULES."/$module/meta.json")) {
263
				$module_meta = file_get_json(MODULES."/$module/meta.json");
264
			}
265
			self::get_dependencies_common_checks($dependencies, $meta, $module_meta);
266
		}
267
		unset($module, $module_meta);
268
		/**
269
		 * Check for compatibility with plugins
270
		 */
271
		foreach ($Config->components['plugins'] as $plugin) {
272
			/**
273
			 * Stub for the case if there is no `meta.json`
274
			 */
275
			$plugin_meta = [
276
				'package'  => $plugin,
277
				'category' => 'plugins',
278
				'version'  => 0
279
			];
280
			if (file_exists(PLUGINS."/$plugin/meta.json")) {
281
				$plugin_meta = file_get_json(PLUGINS."/$plugin/meta.json");
282
			}
283
			self::get_dependencies_common_checks($dependencies, $meta, $plugin_meta);
284
		}
285
		unset($plugin, $plugin_meta);
286
		/**
287
		 * If some required packages still missing
288
		 */
289
		if (!empty($meta['require'])) {
290
			foreach ($meta['require'] as $package => $details) {
291
				$dependencies['require']['unknown'][] = [
292
					'name'     => $package,
293
					'required' => $details
294
				];
295
			}
296
			unset($package, $details);
297
		}
298
		if (!self::check_dependencies_db($meta['db_support'])) {
299
			$dependencies['supported'] = $meta['db_support'];
300
		}
301
		if (!self::check_dependencies_storage($meta['storage_support'])) {
302
			$dependencies['supported'] = $meta['storage_support'];
303
		}
304
		return $dependencies;
305
	}
306
	/**
307
	 * @param array $dependencies
308
	 * @param array $meta
309
	 * @param array $component_meta
310
	 */
311
	protected static function get_dependencies_common_checks (&$dependencies, &$meta, $component_meta) {
312
		$component_meta = self::normalize_meta($component_meta);
313
		$package        = $component_meta['package'];
314
		$category       = $component_meta['category'];
315
		/**
316
		 * Do not compare component with itself
317
		 */
318
		if (self::check_dependencies_are_the_same($meta, $component_meta)) {
319
			if (version_compare($meta['version'], $component_meta['version'], '<')) {
320
				$dependencies['update_older'] = [
321
					'from' => $component_meta['version'],
322
					'to'   => $meta['version']
323
				];
324
				return;
325
			}
326
			/**
327
			 * If update is supported - check whether update is possible from current version
328
			 */
329
			if (
330
				isset($meta['update_from']) &&
331
				version_compare($meta['update_from_version'], $component_meta['version'], '>')
332
			) {
333
				$dependencies['update_from'] = [
334
					'from'            => $component_meta['version'],
335
					'to'              => $meta['version'],
336
					'can_update_from' => $meta['update_from_version']
337
				];
338
			}
339
			return;
340
		}
341
		/**
342
		 * If component already provides the same functionality
343
		 */
344
		if ($already_provided = self::get_dependencies_also_provided_by($meta, $component_meta)) {
345
			$dependencies['provide'][$category][] = [
346
				'name'     => $package,
347
				'features' => $already_provided
348
			];
349
		}
350
		/**
351
		 * Check if component is required and satisfies requirement condition
352
		 */
353
		if ($dependencies_conflicts = self::check_requirement_satisfaction($meta, $component_meta)) {
354
			$dependencies['require'][$category][] = [
355
				'name'     => $package,
356
				'existing' => $component_meta['version'],
357
				'required' => $dependencies_conflicts
358
			];
359
		}
360
		unset($meta['require'][$package]);
361
		/**
362
		 * Satisfy provided required functionality
363
		 */
364
		foreach ($component_meta['provide'] as $p) {
365
			unset($meta['require'][$p]);
366
		}
367
		/**
368
		 * Check for conflicts
369
		 */
370
		if ($dependencies_conflicts = self::get_dependencies_conflicts($meta, $component_meta)) {
371
			$dependencies['conflict'][$category][] = [
372
				'name'      => $package,
373
				'conflicts' => $dependencies_conflicts
374
			];
375
		}
376
	}
377
	/**
378
	 * Check whether there is available supported DB engine
379
	 *
380
	 * @param string[] $db_support
381
	 *
382
	 * @return bool
383
	 */
384
	protected static function check_dependencies_db ($db_support) {
385
		/**
386
		 * Component doesn't support (and thus use) any DB engines, so we don't care what system have
387
		 */
388
		if (!$db_support) {
389
			return true;
390
		}
391
		$Core   = Core::instance();
392
		$Config = Config::instance();
393
		if (in_array($Core->db_type, $db_support)) {
394
			return true;
395
		}
396
		foreach ($Config->db as $database) {
397
			if (isset($database['type']) && in_array($database['type'], $db_support)) {
398
				return true;
399
			}
400
		}
401
		return false;
402
	}
403
	/**
404
	 * Check whether there is available supported Storage engine
405
	 *
406
	 * @param string[] $storage_support
407
	 *
408
	 * @return bool
409
	 */
410
	protected static function check_dependencies_storage ($storage_support) {
411
		/**
412
		 * Component doesn't support (and thus use) any Storage engines, so we don't care what system have
413
		 */
414
		if (!$storage_support) {
415
			return true;
416
		}
417
		$Core   = Core::instance();
418
		$Config = Config::instance();
419
		if (in_array($Core->storage_type, $storage_support)) {
420
			return true;
421
		}
422
		foreach ($Config->storage as $storage) {
423
			if (in_array($storage['connection'], $storage_support)) {
424
				return true;
425
			}
426
		}
427
		return false;
428
	}
429
	/**
430
	 * Check if two both components are the same
431
	 *
432
	 * @param array $new_meta      `meta.json` content of new component
433
	 * @param array $existing_meta `meta.json` content of existing component
434
	 *
435
	 * @return bool
436
	 */
437
	protected static function check_dependencies_are_the_same ($new_meta, $existing_meta) {
438
		return
439
			$new_meta['package'] == $existing_meta['package'] &&
440
			$new_meta['category'] == $existing_meta['category'];
441
	}
442
	/**
443
	 * Check for functionality provided by other components
444
	 *
445
	 * @param array $new_meta      `meta.json` content of new component
446
	 * @param array $existing_meta `meta.json` content of existing component
447
	 *
448
	 * @return array
449
	 */
450
	protected static function get_dependencies_also_provided_by ($new_meta, $existing_meta) {
451
		return array_intersect($new_meta['provide'], $existing_meta['provide']);
452
	}
453
	/**
454
	 * Check whether other component is required and have satisfactory version
455
	 *
456
	 * @param array $new_meta      `meta.json` content of new component
457
	 * @param array $existing_meta `meta.json` content of existing component
458
	 *
459
	 * @return array
460
	 */
461
	protected static function check_requirement_satisfaction ($new_meta, $existing_meta) {
462
		if (
463
			isset($new_meta['require']) &&
464
			$conflicts = self::check_conflicts(
465
				$new_meta['require'],
466
				$existing_meta['package'],
467
				$existing_meta['version']
468
			)
469
		) {
470
			return $conflicts;
471
		}
472
		return [];
473
	}
474
	/**
475
	 * Check whether other component is required and have satisfactory version
476
	 *
477
	 * @param array  $requirements
478
	 * @param string $component
479
	 * @param string $version
480
	 *
481
	 * @return array
482
	 */
483
	protected static function check_conflicts ($requirements, $component, $version) {
484
		/**
485
		 * If we are not interested in component - we are good
486
		 */
487
		if (!isset($requirements[$component])) {
488
			return [];
489
		}
490
		/**
491
		 * Otherwise compare required version with actual present
492
		 */
493
		$conflicts = [];
494
		foreach ($requirements[$component] as $details) {
495
			if (!version_compare($version, $details[1], $details[0])) {
496
				$conflicts[] = $details;
497
			}
498
		}
499
		return $conflicts;
500
	}
501
	/**
502
	 * Check for if component conflicts other components
503
	 *
504
	 * @param array $new_meta      `meta.json` content of new component
505
	 * @param array $existing_meta `meta.json` content of existing component
506
	 *
507
	 * @return array
508
	 */
509
	protected static function get_dependencies_conflicts ($new_meta, $existing_meta) {
510
		/**
511
		 * Check whether two components conflict in any direction by direct conflicts
512
		 */
513
		return array_filter(
514
			[
515
				self::get_dependencies_conflicts_one_step($new_meta, $existing_meta),
516
				self::get_dependencies_conflicts_one_step($existing_meta, $new_meta)
517
			]
518
		);
519
	}
520
	/**
521
	 * @param array $meta_from
522
	 * @param array $meta_to
523
	 *
524
	 * @return array
525
	 */
526
	protected static function get_dependencies_conflicts_one_step ($meta_from, $meta_to) {
527
		if (
528
			isset($meta_from['conflict']) &&
529
			$conflicts = self::check_conflicts(
530
				$meta_from['conflict'],
531
				$meta_to['package'],
532
				$meta_to['version']
533
			)
534
		) {
535
			return [
536
				'package'        => $meta_from['package'],
537
				'conflicts_with' => $meta_to['package'],
538
				'of_versions'    => $conflicts
539
			];
540
		}
541
		return [];
542
	}
543
	/**
544
	 * Check whether package is currently used by any other package (during uninstalling/disabling)
545
	 *
546
	 * @param array $meta `meta.json` contents of target component
547
	 *
548
	 * @return string[][] Empty array if dependencies are fine or array with optional keys `modules` and `plugins` that contain array of dependent packages
549
	 */
550
	static function get_dependent_packages ($meta) {
551
		/**
552
		 * No `meta.json` - nothing to check, allow it
553
		 */
554
		if (!$meta) {
555
			return [];
556
		}
557
		$meta    = self::normalize_meta($meta);
558
		$Config  = Config::instance();
559
		$used_by = [];
560
		/**
561
		 * Checking for backward dependencies of modules
562
		 */
563
		foreach (array_keys($Config->components['modules']) as $module) {
564
			/**
565
			 * If module is not enabled, we compare module with itself or there is no `meta.json` - we do not care about it
566
			 */
567
			if (
568
				(
569
					$meta['category'] == 'modules' &&
570
					$meta['package'] == $module
571
				) ||
572
				!file_exists(MODULES."/$module/meta.json") ||
573
				!$Config->module($module)->enabled()
574
			) {
575
				continue;
576
			}
577
			$module_meta = file_get_json(MODULES."/$module/meta.json");
578
			$module_meta = self::normalize_meta($module_meta);
579
			/**
580
			 * Check if component provided something important here
581
			 */
582
			if (
583
				isset($module_meta['require'][$meta['package']]) ||
584
				array_intersect(array_keys($module_meta['require']), $meta['provide'])
585
			) {
586
				$used_by['modules'][] = $module;
587
			}
588
		}
589
		unset($module);
590
		/**
591
		 * Checking for backward dependencies of plugins
592
		 */
593
		foreach ($Config->components['plugins'] as $plugin) {
594
			/**
595
			 * If we compare plugin with itself or there is no `meta.json` - we do not care about it
596
			 */
597
			if (
598
				(
599
					$meta['category'] == 'plugins' &&
600
					$meta['package'] == $plugin
601
				) ||
602
				!file_exists(PLUGINS."/$plugin/meta.json")
603
			) {
604
				continue;
605
			}
606
			$plugin_meta = file_get_json(PLUGINS."/$plugin/meta.json");
607
			$plugin_meta = self::normalize_meta($plugin_meta);
608
			if (
609
				isset($plugin_meta['require'][$meta['package']]) ||
610
				array_intersect(array_keys($plugin_meta['require']), $meta['provide'])
611
			) {
612
				$used_by['plugins'][] = $plugin;
613
			}
614
		}
615
		return $used_by;
616
	}
617
	/**
618
	 * Normalize structure of `meta.json`
619
	 *
620
	 * Addition necessary items if they are not present and casting some string values to arrays in order to decrease number of checks in further code
621
	 *
622
	 * @param array $meta
623
	 *
624
	 * @return array mixed
625
	 */
626
	protected static function normalize_meta ($meta) {
627
		foreach (['db_support', 'storage_support', 'provide', 'require', 'conflict'] as $item) {
628
			$meta[$item] = isset($meta[$item]) ? (array)$meta[$item] : [];
629
		}
630
		foreach (['require', 'conflict'] as $item) {
631
			$meta[$item] = self::dep_normal($meta[$item]);
632
		}
633
		return $meta;
634
	}
635
	/**
636
	 * Function for normalization of dependencies structure
637
	 *
638
	 * @param array|string $dependence_structure
639
	 *
640
	 * @return array
641
	 */
642
	protected static function dep_normal ($dependence_structure) {
643
		$return = [];
644
		foreach ((array)$dependence_structure as $d) {
645
			preg_match('/^([^<=>!]+)([<=>!]*)(.*)$/', $d, $d);
646
			/** @noinspection NestedTernaryOperatorInspection */
647
			$return[$d[1]][] = [
648
				isset($d[2]) && $d[2] ? str_replace('=>', '>=', $d[2]) : (isset($d[3]) && $d[3] ? '=' : '>='),
649
				isset($d[3]) && $d[3] ? $d[3] : 0
650
			];
651
		}
652
		return $return;
653
	}
654
}
655