Completed
Push — master ( 839a1f...d41ceb )
by Nazar
04:26
created

Packages_manipulation::update_php_sql()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 20
rs 8.8571
cc 5
eloc 9
nc 5
nop 3
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, 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
 * Utility functions, necessary during packages manipulation (installation/uninstallation, enabling/disabling)
19
 */
20
class Packages_manipulation {
21
	/**
22
	 * @param string $file_name File key in `$_FILES` superglobal
23
	 *
24
	 * @return false|string Path to file location if succeed or `false` on failure
25
	 */
26
	static function move_uploaded_file_to_tmp ($file_name) {
27
		if (!isset($_FILES[$file_name]) || !$_FILES[$file_name]['tmp_name']) {
28
			return false;
29
		}
30
		$L    = Language::instance();
31
		$Page = Page::instance();
32
		switch ($_FILES[$file_name]['error']) {
33
			case UPLOAD_ERR_INI_SIZE:
34
			case UPLOAD_ERR_FORM_SIZE:
35
				$Page->warning($L->file_too_large);
36
				return false;
37
			case UPLOAD_ERR_NO_TMP_DIR:
38
				$Page->warning($L->temporary_folder_is_missing);
39
				return false;
40
			case UPLOAD_ERR_CANT_WRITE:
41
				$Page->warning($L->cant_write_file_to_disk);
42
				return false;
43
			case UPLOAD_ERR_PARTIAL:
44
			case UPLOAD_ERR_NO_FILE:
45
				return false;
46
		}
47
		if ($_FILES[$file_name]['error'] != UPLOAD_ERR_OK) {
48
			return false;
49
		}
50
		$tmp_name = TEMP.'/'.md5(random_bytes(1000)).'.phar';
51
		return move_uploaded_file($_FILES[$file_name]['tmp_name'], $tmp_name) ? $tmp_name : false;
52
	}
53
	/**
54
	 * Generic extraction of files from phar distributive for CleverStyle CMS (components installation)
55
	 *
56
	 * @param string $target_directory
57
	 * @param string $source_phar Will be removed after extraction
58
	 *
59
	 * @return bool
60
	 */
61
	static function install_extract ($target_directory, $source_phar) {
62
		if (!mkdir($target_directory, 0770)) {
63
			return false;
64
		}
65
		$tmp_dir   = "phar://$source_phar";
66
		$fs        = file_get_json("$tmp_dir/fs.json");
67
		$extracted = array_filter(
68
			array_map(
69
				function ($index, $file) use ($tmp_dir, $target_directory) {
70
					if (
71
						!file_exists(dirname("$target_directory/$file")) &&
72
						!mkdir(dirname("$target_directory/$file"), 0770, true)
73
					) {
74
						return false;
75
					}
76
					/**
77
					 * TODO: copy() + file_exists() is a hack for HHVM, when bug fixed upstream (copying of empty files) this should be simplified
78
					 */
79
					copy("$tmp_dir/fs/$index", "$target_directory/$file");
80
					return file_exists("$target_directory/$file");
81
				},
82
				$fs,
83
				array_keys($fs)
84
			)
85
		);
86
		unlink($source_phar);
87
		if (count($extracted) === count($fs)) {
88
			file_put_json("$target_directory/fs.json", array_keys($fs));
89
			return true;
90
		}
91
		return false;
92
	}
93
	/**
94
	 * Generic extraction of files from phar distributive for CleverStyle CMS (system and components update)
95
	 *
96
	 * @param string      $target_directory
97
	 * @param string      $source_phar             Will be removed after extraction
98
	 * @param null|string $fs_location_directory   Defaults to `$target_directory`
99
	 * @param null|string $meta_location_directory Defaults to `$target_directory`
100
	 *
101
	 * @return bool
102
	 */
103
	static function update_extract ($target_directory, $source_phar, $fs_location_directory = null, $meta_location_directory = null) {
104
		$fs_location_directory   = $fs_location_directory ?: $target_directory;
105
		$meta_location_directory = $meta_location_directory ?: $target_directory;
106
		/**
107
		 * Backup some necessary information about current version
108
		 */
109
		copy("$fs_location_directory/fs.json", "$fs_location_directory/fs_backup.json");
110
		copy("$meta_location_directory/meta.json", "$meta_location_directory/meta_backup.json");
111
		/**
112
		 * Extracting new versions of files
113
		 */
114
		$tmp_dir   = "phar://$source_phar";
115
		$fs        = file_get_json("$tmp_dir/fs.json");
116
		$extracted = array_filter(
117
			array_map(
118
				function ($index, $file) use ($tmp_dir, $target_directory) {
119
					if (
120
						!file_exists(dirname("$target_directory/$file")) &&
121
						!mkdir(dirname("$target_directory/$file"), 0770, true)
122
					) {
123
						return false;
124
					}
125
					/**
126
					 * TODO: copy() + file_exists() is a hack for HHVM, when bug fixed upstream (copying of empty files) this should be simplified
127
					 */
128
					copy("$tmp_dir/fs/$index", "$target_directory/$file");
129
					return file_exists("$target_directory/$file");
130
				},
131
				$fs,
132
				array_keys($fs)
133
			)
134
		);
135
		unlink($source_phar);
136
		unset($tmp_dir);
137
		if (count($extracted) !== count($fs)) {
138
			return false;
139
		}
140
		unset($extract);
141
		$fs = array_keys($fs);
142
		/**
143
		 * Removing of old unnecessary files and directories
144
		 */
145
		foreach (
146
			array_diff(
147
				file_get_json("$fs_location_directory/fs_backup.json"),
148
				$fs
149
			) as $file
150
		) {
151
			$file = "$target_directory/$file";
152
			if (file_exists($file) && is_writable($file)) {
153
				unlink($file);
154
				// Recursively remove all empty parent directories
155
				while (!get_files_list($file = dirname($file))) {
156
					rmdir($file);
157
				}
158
			}
159
		}
160
		unset($file, $dir);
161
		file_put_json("$fs_location_directory/fs.json", $fs);
162
		/**
163
		 * Removing backups after successful update
164
		 */
165
		unlink("$fs_location_directory/fs_backup.json");
166
		unlink("$meta_location_directory/meta_backup.json");
167
		return true;
168
	}
169
	/**
170
	 * Generic update for CleverStyle CMS (system and components), runs PHP scripts and does DB migrations after extracting of new distributive
171
	 *
172
	 * @param string     $target_directory
173
	 * @param string     $old_version
174
	 * @param array|null $db_array `$Config->components['modules'][$module]['db']` if module or system
175
	 */
176
	static function update_php_sql ($target_directory, $old_version, $db_array = null) {
177
		$meta = file_get_json("$target_directory/meta.json");
178
		if (!$meta['update_versions']) {
179
			return;
180
		}
181
		foreach ($meta['update_versions'] as $version) {
182
			if (version_compare($old_version, $version, '<')) {
183
				/**
184
				 * PHP update script
185
				 */
186
				_include_once("$target_directory/meta/update/$version.php", false);
187
				/**
188
				 * Database update
189
				 */
190
				if ($db_array) {
191
					self::execute_sql_from_directory("$target_directory/meta/update_db", $db_array, $version);
192
				}
193
			}
194
		}
195
	}
196
	/**
197
	 * @param string $directory        Base path to SQL files
198
	 * @param array  $db_configuration Array in form [$db_name => $index]
199
	 * @param string $version          In case when we are working with update script we might have version subdirectory
200
	 */
201
	static function execute_sql_from_directory ($directory, $db_configuration, $version = '') {
202
		$Config = Config::instance();
203
		$Core   = Core::instance();
204
		$db     = DB::instance();
205
		time_limit_pause();
206
		foreach ($db_configuration as $db_name => $index) {
207
			$db_type  = $index == 0 ? $Core->db_type : $Config->db[$index]['type'];
208
			$sql_file = "$directory/$db_name/$version/$db_type.sql";
209
			if (file_exists($sql_file)) {
210
				$db->db_prime($index)->q(
1 ignored issue
show
Bug introduced by
The method q does only exist in cs\DB\_Abstract, but not in cs\False_class.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
211
					explode(';', file_get_contents($sql_file))
212
				);
213
			}
214
		}
215
		time_limit_pause(false);
216
	}
217
	/**
218
	 * Check dependencies for new component (during installation/updating/enabling)
219
	 *
220
	 * @param array $meta `meta.json` contents of target component
221
	 *
222
	 * @return array
223
	 */
224
	static function get_dependencies ($meta) {
225
		/**
226
		 * No `meta.json` - nothing to check, allow it
227
		 */
228
		if (!$meta) {
229
			return [];
230
		}
231
		$meta         = self::normalize_meta($meta);
232
		$Config       = Config::instance();
233
		$dependencies = [];
234
		/**
235
		 * Check for compatibility with modules
236
		 */
237
		foreach (array_keys($Config->components['modules']) as $module) {
238
			/**
239
			 * If module uninstalled - we do not care about it
240
			 */
241
			if ($Config->module($module)->uninstalled()) {
242
				continue;
243
			}
244
			/**
245
			 * Stub for the case if there is no `meta.json`
246
			 */
247
			$module_meta = [
248
				'package'  => $module,
249
				'category' => 'modules',
250
				'version'  => 0
251
			];
252
			if (file_exists(MODULES."/$module/meta.json")) {
253
				$module_meta = file_get_json(MODULES."/$module/meta.json");
254
			}
255
			self::get_dependencies_common_checks($dependencies, $meta, $module_meta);
256
		}
257
		unset($module, $module_meta);
258
		/**
259
		 * Check for compatibility with plugins
260
		 */
261
		foreach ($Config->components['plugins'] as $plugin) {
262
			/**
263
			 * Stub for the case if there is no `meta.json`
264
			 */
265
			$plugin_meta = [
266
				'package'  => $plugin,
267
				'category' => 'plugins',
268
				'version'  => 0
269
			];
270
			if (file_exists(PLUGINS."/$plugin/meta.json")) {
271
				$plugin_meta = file_get_json(PLUGINS."/$plugin/meta.json");
272
			}
273
			self::get_dependencies_common_checks($dependencies, $meta, $plugin_meta);
274
		}
275
		unset($plugin, $plugin_meta);
276
		/**
277
		 * If some required packages still missing
278
		 */
279
		if (!empty($meta['require'])) {
280
			foreach ($meta['require'] as $package => $details) {
281
				$dependencies['require']['unknown'][] = [
282
					'name'     => $package,
283
					'required' => $details
284
				];
285
			}
286
			unset($package, $details);
287
		}
288
		if (!self::check_dependencies_db($meta['db_support'])) {
289
			$dependencies['supported'] = $meta['db_support'];
290
		}
291
		if (!self::check_dependencies_storage($meta['storage_support'])) {
292
			$dependencies['supported'] = $meta['storage_support'];
293
		}
294
		return $dependencies;
295
	}
296
	/**
297
	 * @param array $dependencies
298
	 * @param array $meta
299
	 * @param array $component_meta
300
	 */
301
	protected static function get_dependencies_common_checks (&$dependencies, &$meta, $component_meta) {
302
		$component_meta = self::normalize_meta($component_meta);
303
		$package        = $component_meta['package'];
304
		$category       = $component_meta['category'];
305
		/**
306
		 * Do not compare component with itself
307
		 */
308
		if (self::check_dependencies_are_the_same($meta, $component_meta)) {
309
			if (version_compare($meta['version'], $component_meta['version'], '<')) {
310
				$dependencies['update_older'] = [
311
					'from' => $component_meta['version'],
312
					'to'   => $meta['version']
313
				];
314
				return;
315
			}
316
			/**
317
			 * If update is supported - check whether update is possible from current version
318
			 */
319
			if (
320
				isset($meta['update_from']) &&
321
				version_compare($meta['update_from_version'], $component_meta['version'], '>')
322
			) {
323
				$dependencies['update_from'] = [
324
					'from'            => $component_meta['version'],
325
					'to'              => $meta['version'],
326
					'can_update_from' => $meta['update_from_version']
327
				];
328
			}
329
			return;
330
		}
331
		/**
332
		 * If component already provides the same functionality
333
		 */
334
		if ($already_provided = self::get_dependencies_also_provided_by($meta, $component_meta)) {
335
			$dependencies['provide'][$category][] = [
336
				'name'     => $package,
337
				'features' => $already_provided
338
			];
339
		}
340
		/**
341
		 * Check if component is required and satisfies requirement condition
342
		 */
343
		if ($dependencies_conflicts = self::check_requirement_satisfaction($meta, $component_meta)) {
344
			$dependencies['require'][$category][] = [
345
				'name'     => $package,
346
				'existing' => $component_meta['version'],
347
				'required' => $dependencies_conflicts
348
			];
349
		}
350
		unset($meta['require'][$package]);
351
		/**
352
		 * Satisfy provided required functionality
353
		 */
354
		foreach ($component_meta['provide'] as $p) {
355
			unset($meta['require'][$p]);
356
		}
357
		/**
358
		 * Check for conflicts
359
		 */
360
		if ($dependencies_conflicts = self::get_dependencies_conflicts($meta, $component_meta)) {
361
			$dependencies['conflict'][$category][] = [
362
				'name'      => $package,
363
				'conflicts' => $dependencies_conflicts
364
			];
365
		}
366
	}
367
	/**
368
	 * Check whether there is available supported DB engine
369
	 *
370
	 * @param string[] $db_support
371
	 *
372
	 * @return bool
373
	 */
374
	protected static function check_dependencies_db ($db_support) {
375
		/**
376
		 * Component doesn't support (and thus use) any DB engines, so we don't care what system have
377
		 */
378
		if (!$db_support) {
379
			return true;
380
		}
381
		$Core   = Core::instance();
382
		$Config = Config::instance();
383
		if (in_array($Core->db_type, $db_support)) {
384
			return true;
385
		}
386
		foreach ($Config->db as $database) {
387
			if (isset($database['type']) && in_array($database['type'], $db_support)) {
388
				return true;
389
			}
390
		}
391
		return false;
392
	}
393
	/**
394
	 * Check whether there is available supported Storage engine
395
	 *
396
	 * @param string[] $storage_support
397
	 *
398
	 * @return bool
399
	 */
400
	protected static function check_dependencies_storage ($storage_support) {
401
		/**
402
		 * Component doesn't support (and thus use) any Storage engines, so we don't care what system have
403
		 */
404
		if (!$storage_support) {
405
			return true;
406
		}
407
		$Core   = Core::instance();
408
		$Config = Config::instance();
409
		if (in_array($Core->storage_type, $storage_support)) {
410
			return true;
411
		}
412
		foreach ($Config->storage as $storage) {
413
			if (in_array($storage['connection'], $storage_support)) {
414
				return true;
415
			}
416
		}
417
		return false;
418
	}
419
	/**
420
	 * Check if two both components are the same
421
	 *
422
	 * @param array $new_meta      `meta.json` content of new component
423
	 * @param array $existing_meta `meta.json` content of existing component
424
	 *
425
	 * @return bool
426
	 */
427
	protected static function check_dependencies_are_the_same ($new_meta, $existing_meta) {
428
		return
429
			$new_meta['package'] == $existing_meta['package'] &&
430
			$new_meta['category'] == $existing_meta['category'];
431
	}
432
	/**
433
	 * Check for functionality provided by other components
434
	 *
435
	 * @param array $new_meta      `meta.json` content of new component
436
	 * @param array $existing_meta `meta.json` content of existing component
437
	 *
438
	 * @return array
439
	 */
440
	protected static function get_dependencies_also_provided_by ($new_meta, $existing_meta) {
441
		return array_intersect($new_meta['provide'], $existing_meta['provide']);
442
	}
443
	/**
444
	 * Check whether other component is required and have satisfactory version
445
	 *
446
	 * @param array $new_meta      `meta.json` content of new component
447
	 * @param array $existing_meta `meta.json` content of existing component
448
	 *
449
	 * @return array
450
	 */
451
	protected static function check_requirement_satisfaction ($new_meta, $existing_meta) {
452
		if (
453
			isset($new_meta['require']) &&
454
			$conflicts = self::check_conflicts(
455
				$new_meta['require'],
456
				$existing_meta['package'],
457
				$existing_meta['version']
458
			)
459
		) {
460
			return $conflicts;
461
		}
462
		return [];
463
	}
464
	/**
465
	 * Check whether other component is required and have satisfactory version
466
	 *
467
	 * @param array  $requirements
468
	 * @param string $component
469
	 * @param string $version
470
	 *
471
	 * @return array
472
	 */
473
	protected static function check_conflicts ($requirements, $component, $version) {
474
		/**
475
		 * If we are not interested in component - we are good
476
		 */
477
		if (!isset($requirements[$component])) {
478
			return [];
479
		}
480
		/**
481
		 * Otherwise compare required version with actual present
482
		 */
483
		$conflicts = [];
484
		foreach ($requirements[$component] as $details) {
485
			if (!version_compare($version, $details[1], $details[0])) {
486
				$conflicts[] = $details;
487
			}
488
		}
489
		return $conflicts;
490
	}
491
	/**
492
	 * Check for if component conflicts other components
493
	 *
494
	 * @param array $new_meta      `meta.json` content of new component
495
	 * @param array $existing_meta `meta.json` content of existing component
496
	 *
497
	 * @return array
498
	 */
499
	protected static function get_dependencies_conflicts ($new_meta, $existing_meta) {
500
		/**
501
		 * Check whether two components conflict in any direction by direct conflicts
502
		 */
503
		return array_filter(
504
			[
505
				self::get_dependencies_conflicts_one_step($new_meta, $existing_meta),
506
				self::get_dependencies_conflicts_one_step($existing_meta, $new_meta)
507
			]
508
		);
509
	}
510
	/**
511
	 * @param array $meta_from
512
	 * @param array $meta_to
513
	 *
514
	 * @return array
515
	 */
516
	protected static function get_dependencies_conflicts_one_step ($meta_from, $meta_to) {
517
		if (
518
			isset($meta_from['conflict']) &&
519
			$conflicts = self::check_conflicts(
520
				$meta_from['conflict'],
521
				$meta_to['package'],
522
				$meta_to['version']
523
			)
524
		) {
525
			return [
526
				'package'        => $meta_from['package'],
527
				'conflicts_with' => $meta_to['package'],
528
				'of_versions'    => $conflicts
529
			];
530
		}
531
		return [];
532
	}
533
	/**
534
	 * Check whether package is currently used by any other package (during uninstalling/disabling)
535
	 *
536
	 * @param array $meta `meta.json` contents of target component
537
	 *
538
	 * @return string[][] Empty array if dependencies are fine or array with optional keys `modules` and `plugins` that contain array of dependent packages
539
	 */
540
	static function get_dependent_packages ($meta) {
541
		/**
542
		 * No `meta.json` - nothing to check, allow it
543
		 */
544
		if (!$meta) {
545
			return [];
546
		}
547
		$meta    = self::normalize_meta($meta);
548
		$Config  = Config::instance();
549
		$used_by = [];
550
		/**
551
		 * Checking for backward dependencies of modules
552
		 */
553
		foreach (array_keys($Config->components['modules']) as $module) {
554
			/**
555
			 * If module is not enabled, we compare module with itself or there is no `meta.json` - we do not care about it
556
			 */
557
			if (
558
				(
559
					$meta['category'] == 'modules' &&
560
					$meta['package'] == $module
561
				) ||
562
				!file_exists(MODULES."/$module/meta.json") ||
563
				!$Config->module($module)->enabled()
564
			) {
565
				continue;
566
			}
567
			$module_meta = file_get_json(MODULES."/$module/meta.json");
568
			$module_meta = self::normalize_meta($module_meta);
569
			/**
570
			 * Check if component provided something important here
571
			 */
572
			if (
573
				isset($module_meta['require'][$meta['package']]) ||
574
				array_intersect(array_keys($module_meta['require']), $meta['provide'])
575
			) {
576
				$used_by['modules'][] = $module;
577
			}
578
		}
579
		unset($module);
580
		/**
581
		 * Checking for backward dependencies of plugins
582
		 */
583
		foreach ($Config->components['plugins'] as $plugin) {
584
			/**
585
			 * If we compare plugin with itself or there is no `meta.json` - we do not care about it
586
			 */
587
			if (
588
				(
589
					$meta['category'] == 'plugins' &&
590
					$meta['package'] == $plugin
591
				) ||
592
				!file_exists(PLUGINS."/$plugin/meta.json")
593
			) {
594
				continue;
595
			}
596
			$plugin_meta = file_get_json(PLUGINS."/$plugin/meta.json");
597
			$plugin_meta = self::normalize_meta($plugin_meta);
598
			if (
599
				isset($plugin_meta['require'][$meta['package']]) ||
600
				array_intersect(array_keys($plugin_meta['require']), $meta['provide'])
601
			) {
602
				$used_by['plugins'][] = $plugin;
603
			}
604
		}
605
		return $used_by;
606
	}
607
	/**
608
	 * Normalize structure of `meta.json`
609
	 *
610
	 * 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
611
	 *
612
	 * @param array $meta
613
	 *
614
	 * @return array mixed
615
	 */
616
	protected static function normalize_meta ($meta) {
617
		foreach (['db_support', 'storage_support', 'provide', 'require', 'conflict'] as $item) {
618
			$meta[$item] = isset($meta[$item]) ? (array)$meta[$item] : [];
619
		}
620
		foreach (['require', 'conflict'] as $item) {
621
			$meta[$item] = self::dep_normal($meta[$item]);
622
		}
623
		return $meta;
624
	}
625
	/**
626
	 * Function for normalization of dependencies structure
627
	 *
628
	 * @param array|string $dependence_structure
629
	 *
630
	 * @return array
631
	 */
632
	protected static function dep_normal ($dependence_structure) {
633
		$return = [];
634
		foreach ((array)$dependence_structure as $d) {
635
			preg_match('/^([^<=>!]+)([<=>!]*)(.*)$/', $d, $d);
636
			/** @noinspection NestedTernaryOperatorInspection */
637
			$return[$d[1]][] = [
638
				isset($d[2]) && $d[2] ? str_replace('=>', '>=', $d[2]) : (isset($d[3]) && $d[3] ? '=' : '>='),
639
				isset($d[3]) && $d[3] ? $d[3] : 0
640
			];
641
		}
642
		return $return;
643
	}
644
}
645