Completed
Push — master ( 128713...187a0b )
by Nazar
04:13
created

Packages_dependencies::check_dependencies_db()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 19
rs 8.8571
cc 6
eloc 11
nc 5
nop 1
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
15
/**
16
 * Utility functions, necessary for determining package's dependencies and which packages depend on it
17
 */
18
class Packages_dependencies {
19
	/**
20
	 * Check dependencies for new component (during installation/updating/enabling)
21
	 *
22
	 * @param array $meta `meta.json` contents of target component
23
	 *
24
	 * @return array
25
	 */
26
	static function get_dependencies ($meta) {
27
		/**
28
		 * No `meta.json` - nothing to check, allow it
29
		 */
30
		if (!$meta) {
31
			return [];
32
		}
33
		$meta         = self::normalize_meta($meta);
34
		$Config       = Config::instance();
35
		$dependencies = [];
36
		/**
37
		 * Check for compatibility with modules
38
		 */
39
		foreach (array_keys($Config->components['modules']) as $module) {
40
			/**
41
			 * If module uninstalled - we do not care about it
42
			 */
43
			if ($Config->module($module)->uninstalled()) {
44
				continue;
45
			}
46
			/**
47
			 * Stub for the case if there is no `meta.json`
48
			 */
49
			$module_meta = [
50
				'package'  => $module,
51
				'category' => 'modules',
52
				'version'  => 0
53
			];
54
			if (file_exists(MODULES."/$module/meta.json")) {
55
				$module_meta = file_get_json(MODULES."/$module/meta.json");
56
			}
57
			self::common_checks($dependencies, $meta, $module_meta);
58
		}
59
		unset($module, $module_meta);
60
		/**
61
		 * Check for compatibility with plugins
62
		 */
63
		foreach ($Config->components['plugins'] as $plugin) {
64
			/**
65
			 * Stub for the case if there is no `meta.json`
66
			 */
67
			$plugin_meta = [
68
				'package'  => $plugin,
69
				'category' => 'plugins',
70
				'version'  => 0
71
			];
72
			if (file_exists(PLUGINS."/$plugin/meta.json")) {
73
				$plugin_meta = file_get_json(PLUGINS."/$plugin/meta.json");
74
			}
75
			self::common_checks($dependencies, $meta, $plugin_meta);
76
		}
77
		unset($plugin, $plugin_meta);
78
		/**
79
		 * If some required packages still missing
80
		 */
81
		foreach ($meta['require'] as $package => $details) {
82
			$dependencies['require']['unknown'][] = [
83
				'name'     => $package,
84
				'required' => $details
85
			];
86
		}
87
		unset($package, $details);
88
		if (!self::check_dependencies_db($meta['db_support'])) {
89
			$dependencies['db_support'] = $meta['db_support'];
90
		}
91
		if (!self::check_dependencies_storage($meta['storage_support'])) {
92
			$dependencies['storage_support'] = $meta['storage_support'];
93
		}
94
		return $dependencies;
95
	}
96
	/**
97
	 * @param array $dependencies
98
	 * @param array $meta
99
	 * @param array $component_meta
100
	 */
101
	protected static function common_checks (&$dependencies, &$meta, $component_meta) {
102
		$component_meta = self::normalize_meta($component_meta);
103
		$package        = $component_meta['package'];
104
		$category       = $component_meta['category'];
105
		/**
106
		 * Do not compare component with itself
107
		 */
108
		if (self::check_dependencies_are_the_same($meta, $component_meta)) {
109
			if (version_compare($meta['version'], $component_meta['version'], '<')) {
110
				$dependencies['update_older'] = [
111
					'from' => $component_meta['version'],
112
					'to'   => $meta['version']
113
				];
114
				return;
115
			}
116
			/**
117
			 * If update is supported - check whether update is possible from current version
118
			 */
119
			if (
120
				isset($meta['update_from']) &&
121
				version_compare($meta['update_from_version'], $component_meta['version'], '>')
122
			) {
123
				$dependencies['update_from'] = [
124
					'from'            => $component_meta['version'],
125
					'to'              => $meta['version'],
126
					'can_update_from' => $meta['update_from_version']
127
				];
128
			}
129
			return;
130
		}
131
		/**
132
		 * If component already provides the same functionality
133
		 */
134
		if ($already_provided = self::also_provided_by($meta, $component_meta)) {
135
			$dependencies['provide'][$category][] = [
136
				'name'     => $package,
137
				'features' => $already_provided
138
			];
139
		}
140
		/**
141
		 * Check if component is required and satisfies requirement condition
142
		 */
143
		if ($dependencies_conflicts = self::check_requirement_satisfaction($meta, $component_meta)) {
144
			$dependencies['require'][$category][] = [
145
				'name'     => $package,
146
				'existing' => $component_meta['version'],
147
				'required' => $dependencies_conflicts
148
			];
149
		}
150
		unset($meta['require'][$package]);
151
		/**
152
		 * Satisfy provided required functionality
153
		 */
154
		foreach ($component_meta['provide'] as $p) {
155
			unset($meta['require'][$p]);
156
		}
157
		/**
158
		 * Check for conflicts
159
		 */
160
		if ($dependencies_conflicts = self::conflicts($meta, $component_meta)) {
161
			$dependencies['conflict'][$category][] = [
162
				'name'      => $package,
163
				'conflicts' => $dependencies_conflicts
164
			];
165
		}
166
	}
167
	/**
168
	 * Check whether there is available supported DB engine
169
	 *
170
	 * @param string[] $db_support
171
	 *
172
	 * @return bool
173
	 */
174
	protected static function check_dependencies_db ($db_support) {
175
		/**
176
		 * Component doesn't support (and thus use) any DB engines, so we don't care what system have
177
		 */
178
		if (!$db_support) {
179
			return true;
180
		}
181
		$Core   = Core::instance();
182
		$Config = Config::instance();
183
		if (in_array($Core->db_type, $db_support)) {
184
			return true;
185
		}
186
		foreach ($Config->db as $database) {
187
			if (isset($database['type']) && in_array($database['type'], $db_support)) {
188
				return true;
189
			}
190
		}
191
		return false;
192
	}
193
	/**
194
	 * Check whether there is available supported Storage engine
195
	 *
196
	 * @param string[] $storage_support
197
	 *
198
	 * @return bool
199
	 */
200
	protected static function check_dependencies_storage ($storage_support) {
201
		/**
202
		 * Component doesn't support (and thus use) any Storage engines, so we don't care what system have
203
		 */
204
		if (!$storage_support) {
205
			return true;
206
		}
207
		$Core   = Core::instance();
208
		$Config = Config::instance();
209
		if (in_array($Core->storage_type, $storage_support)) {
210
			return true;
211
		}
212
		foreach ($Config->storage as $storage) {
213
			if (in_array($storage['connection'], $storage_support)) {
214
				return true;
215
			}
216
		}
217
		return false;
218
	}
219
	/**
220
	 * Check if two both components are the same
221
	 *
222
	 * @param array $new_meta      `meta.json` content of new component
223
	 * @param array $existing_meta `meta.json` content of existing component
224
	 *
225
	 * @return bool
226
	 */
227
	protected static function check_dependencies_are_the_same ($new_meta, $existing_meta) {
228
		return
229
			$new_meta['package'] == $existing_meta['package'] &&
230
			$new_meta['category'] == $existing_meta['category'];
231
	}
232
	/**
233
	 * Check for functionality provided by other components
234
	 *
235
	 * @param array $new_meta      `meta.json` content of new component
236
	 * @param array $existing_meta `meta.json` content of existing component
237
	 *
238
	 * @return array
239
	 */
240
	protected static function also_provided_by ($new_meta, $existing_meta) {
241
		return array_intersect($new_meta['provide'], $existing_meta['provide']);
242
	}
243
	/**
244
	 * Check whether other component is required and have satisfactory version
245
	 *
246
	 * @param array $new_meta      `meta.json` content of new component
247
	 * @param array $existing_meta `meta.json` content of existing component
248
	 *
249
	 * @return array
250
	 */
251
	protected static function check_requirement_satisfaction ($new_meta, $existing_meta) {
252
		return self::check_conflicts($new_meta['require'], $existing_meta['package'], $existing_meta['version']);
253
	}
254
	/**
255
	 * Check whether other component is required and have satisfactory version
256
	 *
257
	 * @param array  $requirements
258
	 * @param string $component
259
	 * @param string $version
260
	 *
261
	 * @return array
262
	 */
263
	protected static function check_conflicts ($requirements, $component, $version) {
264
		/**
265
		 * If we are not interested in component - we are good
266
		 */
267
		if (!isset($requirements[$component])) {
268
			return [];
269
		}
270
		/**
271
		 * Otherwise compare required version with actual present
272
		 */
273
		$conflicts = [];
274
		foreach ($requirements[$component] as $details) {
275
			if (!version_compare($version, $details[1], $details[0])) {
276
				$conflicts[] = $details;
277
			}
278
		}
279
		return $conflicts;
280
	}
281
	/**
282
	 * Check for if component conflicts other components
283
	 *
284
	 * @param array $new_meta      `meta.json` content of new component
285
	 * @param array $existing_meta `meta.json` content of existing component
286
	 *
287
	 * @return array
288
	 */
289
	protected static function conflicts ($new_meta, $existing_meta) {
290
		/**
291
		 * Check whether two components conflict in any direction by direct conflicts
292
		 */
293
		return array_filter(
294
			[
295
				self::conflicts_one_step($new_meta, $existing_meta),
296
				self::conflicts_one_step($existing_meta, $new_meta)
297
			]
298
		);
299
	}
300
	/**
301
	 * @param array $meta_from
302
	 * @param array $meta_to
303
	 *
304
	 * @return array
305
	 */
306
	protected static function conflicts_one_step ($meta_from, $meta_to) {
307
		$conflicts = self::check_conflicts($meta_from['conflict'], $meta_to['package'], $meta_to['version']);
308
		if ($conflicts) {
309
			return [
310
				'package'        => $meta_from['package'],
311
				'conflicts_with' => $meta_to['package'],
312
				'of_versions'    => $conflicts
313
			];
314
		}
315
		return [];
316
	}
317
	/**
318
	 * Check whether package is currently used by any other package (during uninstalling/disabling)
319
	 *
320
	 * @param array $meta `meta.json` contents of target component
321
	 *
322
	 * @return string[][] Empty array if dependencies are fine or array with optional keys `modules` and `plugins` that contain array of dependent packages
323
	 */
324
	static function get_dependent_packages ($meta) {
325
		/**
326
		 * No `meta.json` - nothing to check, allow it
327
		 */
328
		if (!$meta) {
329
			return [];
330
		}
331
		$meta    = self::normalize_meta($meta);
332
		$Config  = Config::instance();
333
		$used_by = [];
334
		/**
335
		 * Checking for backward dependencies of modules
336
		 */
337
		foreach (array_keys($Config->components['modules']) as $module) {
338
			/**
339
			 * If module is not enabled, we compare module with itself or there is no `meta.json` - we do not care about it
340
			 */
341
			if (
342
				(
343
					$meta['category'] == 'modules' &&
344
					$meta['package'] == $module
345
				) ||
346
				!file_exists(MODULES."/$module/meta.json") ||
347
				!$Config->module($module)->enabled()
348
			) {
349
				continue;
350
			}
351
			$module_meta = file_get_json(MODULES."/$module/meta.json");
352
			$module_meta = self::normalize_meta($module_meta);
353
			/**
354
			 * Check if component provided something important here
355
			 */
356
			if (
357
				isset($module_meta['require'][$meta['package']]) ||
358
				array_intersect(array_keys($module_meta['require']), $meta['provide'])
359
			) {
360
				$used_by['modules'][] = $module;
361
			}
362
		}
363
		unset($module);
364
		/**
365
		 * Checking for backward dependencies of plugins
366
		 */
367
		foreach ($Config->components['plugins'] as $plugin) {
368
			/**
369
			 * If we compare plugin with itself or there is no `meta.json` - we do not care about it
370
			 */
371
			if (
372
				(
373
					$meta['category'] == 'plugins' &&
374
					$meta['package'] == $plugin
375
				) ||
376
				!file_exists(PLUGINS."/$plugin/meta.json")
377
			) {
378
				continue;
379
			}
380
			$plugin_meta = file_get_json(PLUGINS."/$plugin/meta.json");
381
			$plugin_meta = self::normalize_meta($plugin_meta);
382
			if (
383
				isset($plugin_meta['require'][$meta['package']]) ||
384
				array_intersect(array_keys($plugin_meta['require']), $meta['provide'])
385
			) {
386
				$used_by['plugins'][] = $plugin;
387
			}
388
		}
389
		return $used_by;
390
	}
391
	/**
392
	 * Normalize structure of `meta.json`
393
	 *
394
	 * 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
395
	 *
396
	 * @param array $meta
397
	 *
398
	 * @return array mixed
399
	 */
400
	protected static function normalize_meta ($meta) {
401
		foreach (['db_support', 'storage_support', 'provide', 'require', 'conflict'] as $item) {
402
			$meta[$item] = isset($meta[$item]) ? (array)$meta[$item] : [];
403
		}
404
		foreach (['require', 'conflict'] as $item) {
405
			$meta[$item] = self::dep_normal($meta[$item]);
406
		}
407
		return $meta;
408
	}
409
	/**
410
	 * Function for normalization of dependencies structure
411
	 *
412
	 * @param array|string $dependence_structure
413
	 *
414
	 * @return array
415
	 */
416
	protected static function dep_normal ($dependence_structure) {
417
		$return = [];
418
		foreach ((array)$dependence_structure as $d) {
419
			preg_match('/^([^<=>!]+)([<=>!]*)(.*)$/', $d, $d);
420
			/** @noinspection NestedTernaryOperatorInspection */
421
			$return[$d[1]][] = [
422
				isset($d[2]) && $d[2] ? $d[2] : (isset($d[3]) && $d[3] ? '=' : '>='),
423
				isset($d[3]) && $d[3] ? $d[3] : 0
424
			];
425
		}
426
		return $return;
427
	}
428
}
429