Passed
Push — master ( ea0156...5f4027 )
by Justin
34:38 queued 30:38
created

PluginInstaller::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 2
rs 10
cc 1
eloc 1
nc 1
nop 1
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 plugin is already installed
234
		if ($this->plugin->isInstalled()) {
235
			return false;
236
		}
237
238
		//check, if install.json is used
239
		if ($this->plugin->hasInstallJson()) {
240
			//check, if install.json exists
241
			if (!file_exists($this->plugin->getPath() . "install.json")) {
242
				throw new IllegalStateException("plugin '" . $this->plugin->getName() . "' requires a install.json, but plugin directory doesnt contains a install.json file.");
243
			}
244
245
			//get content
246
			$install_json = json_decode(file_get_contents($this->plugin->getPath() . "install.json"), true);
247
248
			$installer_plugins = self::listInstallerPlugins();
249
250
			var_dump($installer_plugins);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($installer_plugins) looks like debug code. Are you sure you do not want to remove it?
Loading history...
251
252
			foreach ($installer_plugins as $i_plugin) {
253
				//cast plugin
254
				$i_plugin = PluginInstaller_Plugin::cast($i_plugin);
255
256
				//execute installer plugin
257
				$i_plugin->install($this->plugin, $install_json);
258
			}
259
260
			if (isset($install_json['install_script'])) {
261
				$script_filename = $install_json['install_script'];
262
				$script_path = $this->plugin->getPath() . $script_filename;
263
264
				//execute script, if exists
265
				if (file_exists($script_path)) {
266
					require($script_path);
267
				} else {
268
					throw new IllegalStateException("a install script '" . $script_filename . "' is set for plugin '" . $this->plugin->getName() . "', but file doesnt exists (path: " . $script_path . ").");
269
				}
270
			}
271
		}
272
273
		//set plugin as installed
274
		$this->setInstalled();
275
276
		//clear cache
277
		Cache::clear();
278
279
		return true;
280
	}
281
282
	public function uninstall () : bool {
283
		//check, if install.json is used
284
		if ($this->plugin->hasInstallJson()) {
285
			//check, if install.json exists
286
			if (!file_exists($this->plugin->getPath() . "install.json")) {
287
				throw new IllegalStateException("plugin '" . $this->plugin->getName() . "' requires a install.json, but plugin directory doesnt contains a install.json file.");
288
			}
289
290
			//get content
291
			$install_json = json_decode(file_get_contents($this->plugin->getPath() . "install.json"), true);
292
293
			$installer_plugins = self::listInstallerPlugins();
294
295
			foreach ($installer_plugins as $i_plugin) {
296
				//cast plugin
297
				$i_plugin = PluginInstaller_Plugin::cast($i_plugin);
298
299
				//execute installer plugin
300
				$i_plugin->uninstall($this->plugin, $install_json);
301
			}
302
303
			if (isset($install_json['uninstall_script'])) {
304
				$script_filename = $install_json['uninstall_script'];
305
				$script_path = $this->plugin->getPath() . $script_filename;
306
307
				//execute script, if exists
308
				if (file_exists($script_path)) {
309
					require($script_path);
310
				} else {
311
					throw new IllegalStateException("a uninstall script '" . $script_filename . "' is set for plugin '" . $this->plugin->getName() . "', but file doesnt exists (path: " . $script_path . ").");
312
				}
313
			}
314
		}
315
316
		//set plugin as uninstalled
317
		$this->setUnInstalled();
318
319
		//clear cache
320
		Cache::clear();
321
322
		return true;
323
	}
324
325
	public function upgrade () {
326
		throw new Exception("UnuspportedOperationException: method PluginInstaller::upgrade() isnt implemented yet.");
327
	}
328
329
	public function setInstalled () {
330
		Database::getInstance()->execute("INSERT INTO `{praefix}plugins` (
331
			`name`, `version`, `installed`, `activated`
332
		) VALUES (
333
			:name, :version, :installed, :activated
334
		) ON DUPLICATE KEY UPDATE `installed` = '1', `version` = :version; ", array(
335
			'name' => $this->plugin->getName(),
336
			'version' => $this->plugin->getVersion(),
337
			'installed' => 1,
338
			'activated' => 0
339
		));
340
341
		//clear cache
342
		Plugins::clearCache();
343
	}
344
345
	public function setUnInstalled () {
346
		Database::getInstance()->execute("DELETE FROM `{praefix}plugins` WHERE `name` = :name; ", array(
347
			'name' => $this->plugin->getName()
348
		));
349
350
		//clear cache
351
		Plugins::clearCache();
352
	}
353
354
	public static function listInstallerPlugins () : array {
355
		$rows = array();
356
357
		if (Cache::contains("plugins", "installer_plugins")) {
358
			$rows = Cache::get("plugins", "installer_plugins");
359
		} else {
360
			$rows = Database::getInstance()->listRows("SELECT * FROM `{praefix}plugin_installer_plugins`; ");
361
362
			//put into cache
363
			Cache::put("plugins", "installer_plugins", $rows);
364
		}
365
366
		$plugins = array();
367
368
		foreach ($rows as $row) {
369
			$class_name = $row['class_name'];
370
			$path = $row['path'];
371
372
			//first, include path
373
			require_once(ROOT_PATH . $path);
374
375
			//create object
376
			$obj = new $class_name();
377
378
			//check, if object extends PluginInstaller_Plugin
379
			if ($obj instanceof PluginInstaller_Plugin) {
380
				//add plugin to list
381
				$plugins[] = $obj;
382
			}
383
		}
384
385
		//sort list
386
		usort($plugins, function(PluginInstaller_Plugin $a, PluginInstaller_Plugin $b) {
387
			return $a->getPriority() <=> $b->getPriority();
388
		});
389
390
		return $plugins;
391
	}
392
393
	public static function addInstallerPluginIfAbsent (string $class_name, string $path) {
394
		Database::getInstance()->execute("INSERT INTO `{praefix}plugin_installer_plugins` (
395
			`class_name`, `path`
396
		) VALUES (
397
			:class_name, :path
398
		) ON DUPLICATE KEY UPDATE `path` = :path; ", array(
399
			'class_name' => $class_name,
400
			'path' => $path
401
		));
402
403
		//clear cache
404
		Cache::clear("plugins", "installer_plugins");
405
	}
406
407
	public static function removeInstallerPlugin (string $class_name) {
408
		Database::getInstance()->execute("DELETE FROM `{praefix}plugin_installer_plugins` WHERE `class_name` = :class_name; ", array(
409
			'class_name' => $class_name
410
		));
411
412
		//clear cache
413
		Cache::clear("plugins", "installer_plugins");
414
	}
415
416
}
417
418
?>
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...
419