Test Failed
Push — master ( 9caba9...decc91 )
by Justin
04:21
created

PluginInstaller::getPluginVersion()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 12
rs 9.4285
cc 3
eloc 7
nc 4
nop 2
1
<?php
2
3
/**
4
 * Copyright (c) 2018 Justin Kuenzel (jukusoft.com)
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
20
/**
21
 * Project: JuKuCMS
22
 * License: Apache 2.0 license
23
 * User: Justin
24
 * Date: 08.04.2018
25
 * Time: 14:45
26
 */
27
28
class PluginInstaller {
29
30
	//plugin to install / deinstall
31
	protected $plugin = null;
32
33
	public function __construct(Plugin $plugin) {
34
		$this->plugin = $plugin;
35
	}
36
37
	/**
38
	 * check php version, php extensions and so on
39
	 *
40
	 * @return mixed true, if all required plugins are available, or an array with missing
41
	 */
42
	public function checkRequirements (bool $dontCheckPlugins = false) {
43
		//get require
44
		$require_array = $this->plugin->getRequiredPlugins();
45
46
		//get package list
47
		require(STORE_PATH . "package_list.php");
48
49
		$missing_plugins = array();
50
51
		$installed_plugins = Plugins::listInstalledPlugins();
0 ignored issues
show
Unused Code introduced by
The assignment to $installed_plugins is dead and can be removed.
Loading history...
52
53
		//iterate through all requirements
54
		foreach ($require_array as $requirement=>$version) {
55
			if ($requirement === "php") {
56
				//check php version
57
				if (!$this->checkVersion($version, phpversion())) {
58
					$missing_plugins[] = $requirement;
59
60
					continue;
61
				}
62
			} else if (PHPUtils::startsWith($requirement, "ext-")) {
63
				//check php extension
64
65
				$extension = str_replace("ext-", "", $requirement);
66
67
				//check, if php extension is loaded
68
				if (!extension_loaded($extension)) {
69
					$missing_plugins[] = $requirement;
70
71
					continue;
72
				}
73
74
				//get extension version
75
				$current_version = phpversion($extension);
76
77
				//check version
78
				if (!$this->checkVersion($version, $current_version)) {
79
					$missing_plugins[] = $requirement;
80
				}
81
			} else if (PHPUtils::startsWith($requirement, "apache-")) {
82
				//check for apache module, but no version check is supported
83
84
				$module = str_replace("apache-", "", $requirement);
85
86
				if (!function_exists('apache_get_modules')) {
87
					$missing_plugins[] = "apache";
88
89
					continue;
90
				}
91
92
				if (!in_array($module, apache_get_modules())) {
93
					$missing_plugins[] = $requirement;
94
				}
95
			} else if (PHPUtils::startsWith($requirement, "package-")) {
96
				//check if package is installed
97
				$package = str_replace("package-", "", $requirement);
98
99
				//packages doesnt supports specific version
100
101
				if (!isset($package_list[$package])) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $package_list does not exist. Did you maybe mean $package?
Loading history...
102
					$missing_plugins[] = $requirement;
103
				}
104
			} else if ($requirement === "core") {
105
				//check core version
106
				if ($version === "*") {
107
					//we dont have to check version
108
				} else {
109
					//get current version
110
					$array = explode(" ", Version::current()->getVersion());
111
					$current_core_version = $array[0];
112
113
					//check version
114
					if (!$this->checkVersion($version, $current_core_version)) {
115
						$missing_plugins[] = "core";
116
					}
117
				}
118
			} else {
119
				throw new Exception("plugin requirement check isnt supported yet.");
120
121
				if (!$dontCheckPlugins) {
0 ignored issues
show
Unused Code introduced by
IfNode is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
122
					continue;
123
				}
124
125
				//check, if plugin is installed
126
				if (!self::isPluginInstalled($requirement, $installed_plugins)) {
127
					$missing_plugins[] = $requirement;
128
					continue;
129
				}
130
131
				//check plugin version
132
				$current_version = self::getPluginVersion($requirement, $installed_plugins);
133
134
				//check version
135
				if (!$this->checkVersion($version, $current_version)) {
136
					$missing_plugins[] = $requirement . ":version";
137
				}
138
			}
139
		}
140
141
		if (empty($missing_plugins)) {
142
			return true;
143
		} else {
144
			return $missing_plugins;
145
		}
146
	}
147
148
	public static function isPluginInstalled (string $plugin_name, array $installed_plugins = array()) : bool {
149
		if (empty($installed_plugins)) {
150
			$installed_plugins = Plugins::listInstalledPlugins();
151
		}
152
153
		if (!isset($installed_plugins[$plugin_name])) {
154
			//plugin is not installed
155
			return false;
156
		} else {
157
			//plugin is installed
158
			return true;
159
		}
160
	}
161
162
	public static function getPluginVersion (string $plugin_name, array $installed_plugins = array()) {
163
		if (empty($installed_plugins)) {
164
			$installed_plugins = Plugins::listInstalledPlugins();
165
		}
166
167
		if (!isset($installed_plugins[$plugin_name])) {
168
			//plugin is not installed
169
			return false;
170
		} else {
171
			//plugin is installed, return installed version
172
			$plugin = Plugin::castPlugin($installed_plugins[$plugin_name]);
173
			return $plugin->getInstalledVersion();
174
		}
175
	}
176
177
	protected function checkVersion (string $expected_version, $current_version) : bool {
178
		//remove alpha and beta labels
179
		/*$expected_version = str_replace("-alpha", "", $expected_version);
180
		$expected_version = str_replace("-beta", "", $expected_version);
181
		$current_version = str_replace("-alpha", "", $current_version);
182
		$current_version = str_replace("-beta", "", $current_version);*/
183
184
		//validate version numbers, remove suffixes "-alpha", "-beta" and such like -1~dotdeb+8.1 (PHP version 7.0.29-1~dotdeb+8.1)
185
		$array1 = explode("-", $expected_version);
186
		$expected_version = $array1[0];
187
188
		$array2 = explode("-", $current_version);
189
		$current_version = $array2[0];
190
191
		//check version
192
		if (is_numeric($expected_version)) {
193
			//a specific version is required
194
			if ($current_version !== $expected_version) {
195
				return false;
196
			} else {
197
				return true;
198
			}
199
		} else if ($expected_version === "*") {
200
			//every version is allowed
201
			return true;
202
		} else {
203
			//parse version string
204
205
			$operator_length = 0;
206
207
			for ($i = 0; $i < strlen($expected_version); $i++) {
208
				if (!is_numeric($expected_version[$i])) {
209
					$operator_length++;
210
				} else {
211
					break;
212
				}
213
			}
214
215
			//get operator and version
216
			$operator = substr($expected_version, 0, $operator_length);
217
			$version = substr($expected_version, $operator_length);
218
219
			if (!empty($operator_length)) {
220
				return version_compare($current_version, $version, $operator) === TRUE;
221
			} else {
222
				return version_compare($current_version, $expected_version) === 0;
223
			}
224
		}
225
	}
226
227
	public function install () : bool {
228
		//first, check compatibility
229
		if (!$this->checkRequirements()) {
230
			return false;
231
		}
232
233
		//check, if install.json is used
234
		if ($this->plugin->hasInstallJson()) {
235
			//check, if install.json exists
236
			if (!file_exists($this->plugin->getPath() . "install.json")) {
237
				throw new IllegalStateException("plugin '" . $this->plugin->getName() . "' requires a install.json, but plugin directory doesnt contains a install.json file.");
238
			}
239
240
			//get content
241
			$install_json = json_decode(file_get_contents($this->plugin->getPath() . "install.json"), true);
242
243
			$installer_plugins = self::listInstallerPlugins();
244
245
			foreach ($installer_plugins as $i_plugin) {
246
				//cast plugin
247
				$i_plugin = PluginInstaller_Plugin::cast($i_plugin);
248
249
				//execute installer plugin
250
				$i_plugin->install($this->plugin, $installer_plugins);
251
			}
252
253
			if (isset($install_json['install_script'])) {
254
				$script_filename = $install_json['install_script'];
255
				$script_path = $this->plugin->getPath() . $script_filename;
256
257
				//execute script, if exists
258
				if (file_exists($script_path)) {
259
					require($script_path);
260
				} else {
261
					throw new IllegalStateException("a install script '" . $script_filename . "' is set for plugin '" . $this->plugin->getName() . "', but file doesnt exists (path: " . $script_path . ").");
262
				}
263
			}
264
		}
265
266
		//set plugin as installed
267
		$this->setInstalled();
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return boolean. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
268
	}
269
270
	public function uninstall () : bool {
271
		//check, if install.json is used
272
		if ($this->plugin->hasInstallJson()) {
273
			//check, if install.json exists
274
			if (!file_exists($this->plugin->getPath() . "install.json")) {
275
				throw new IllegalStateException("plugin '" . $this->plugin->getName() . "' requires a install.json, but plugin directory doesnt contains a install.json file.");
276
			}
277
278
			//get content
279
			$install_json = json_decode(file_get_contents($this->plugin->getPath() . "install.json"), true);
280
281
			$installer_plugins = self::listInstallerPlugins();
282
283
			foreach ($installer_plugins as $i_plugin) {
284
				//cast plugin
285
				$i_plugin = PluginInstaller_Plugin::cast($i_plugin);
286
287
				//execute installer plugin
288
				$i_plugin->uninstall($this->plugin, $installer_plugins);
289
			}
290
291
			if (isset($install_json['uninstall_script'])) {
292
				$script_filename = $install_json['uninstall_script'];
293
				$script_path = $this->plugin->getPath() . $script_filename;
294
295
				//execute script, if exists
296
				if (file_exists($script_path)) {
297
					require($script_path);
298
				} else {
299
					throw new IllegalStateException("a uninstall script '" . $script_filename . "' is set for plugin '" . $this->plugin->getName() . "', but file doesnt exists (path: " . $script_path . ").");
300
				}
301
			}
302
		}
303
304
		//set plugin as uninstalled
305
		$this->setUnInstalled();
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return boolean. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
306
	}
307
308
	public function upgrade () {
309
		throw new Exception("UnuspportedOperationException: method PluginInstaller::upgrade() isnt implemented yet.");
310
	}
311
312
	public function setInstalled () {
313
		Database::getInstance()->execute("INSERT INTO `{praefix}plugins` (
314
			`name`, `version`, `installed`, `activated`
315
		) VALUES (
316
			:name, :version, :installed, :activated
317
		) ON DUPLICATE KEY UPDATE `installed` = '1', `version` = :version; ", array(
318
			'name' => $this->plugin->getName(),
319
			'version' => $this->plugin->getVersion(),
320
			'installed' => 1,
321
			'activated' => 0
322
		));
323
324
		//clear cache
325
		Plugins::clearCache();
326
	}
327
328
	public function setUnInstalled () {
329
		Database::getInstance()->execute("DELETE FROM `{praefix}plugins` WHERE `name` = :name; ", array(
330
			'name' => $this->plugin->getName()
331
		));
332
333
		//clear cache
334
		Plugins::clearCache();
335
	}
336
337
	public static function listInstallerPlugins () : array {
338
		$rows = array();
339
340
		if (Cache::contains("plugins", "installer_plugins")) {
341
			$rows = Cache::get("plugins", "installer_plugins");
342
		} else {
343
			$rows = Database::getInstance()->listRows("SELECT * FROM `{praefix}plugin_installer_plugins`; ");
344
345
			//put into cache
346
			Cache::put("plugins", "installer_plugins", $rows);
347
		}
348
349
		$plugins = array();
350
351
		foreach ($rows as $row) {
352
			$class_name = $row['class_name'];
353
			$path = $row['path'];
354
355
			//first, include path
356
			require_once(ROOT_PATH . $path);
357
358
			//create object
359
			$obj = new $class_name();
360
361
			//check, if object extends PluginInstaller_Plugin
362
			if ($obj instanceof PluginInstaller_Plugin) {
363
				//add plugin to list
364
				$plugins[] = $obj;
365
			}
366
		}
367
368
		//sort list
369
		usort($plugins, function(PluginInstaller_Plugin $a, PluginInstaller_Plugin $b) {
370
			return $a->getPriority() <=> $b->getPriority();
371
		});
372
373
		return $plugins;
374
	}
375
376
	public static function addInstallerPluginIfAbsent (string $class_name, string $path) {
377
		Database::getInstance()->execute("INSERT INTO `{praefix}plugin_installer_plugins` (
378
			`class_name`, `path`
379
		) VALUES (
380
			:class_name, :path
381
		) ON DUPLICATE KEY UPDATE `path` = :path; ", array(
382
			'class_name' => $class_name,
383
			'path' => $path
384
		));
385
386
		//clear cache
387
		Cache::clear("plugins", "installer_plugins");
388
	}
389
390
	public static function removeInstallerPlugin (string $class_name) {
391
		Database::getInstance()->execute("DELETE FROM `{praefix}plugin_installer_plugins` WHERE `class_name` = :class_name; ", array(
392
			'class_name' => $class_name
393
		));
394
395
		//clear cache
396
		Cache::clear("plugins", "installer_plugins");
397
	}
398
399
}
400
401
?>
0 ignored issues
show
Best Practice introduced by
It is not recommended to use PHP's closing tag ?> in files other than templates.

Using a closing tag in PHP files that only contain PHP code is not recommended as you might accidentally add whitespace after the closing tag which would then be output by PHP. This can cause severe problems, for example headers cannot be sent anymore.

A simple precaution is to leave off the closing tag as it is not required, and it also has no negative effects whatsoever.

Loading history...
402