Completed
Push — master ( 4a9cb8...f72f55 )
by Morris
12:46
created

Installer::installAppBundle()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 11
nc 3
nop 1
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 * @copyright Copyright (c) 2016, Lukas Reschke <[email protected]>
5
 *
6
 * @author Arthur Schiwon <[email protected]>
7
 * @author Bart Visscher <[email protected]>
8
 * @author Brice Maron <[email protected]>
9
 * @author Christian Weiske <[email protected]>
10
 * @author Christopher Schäpers <[email protected]>
11
 * @author Frank Karlitschek <[email protected]>
12
 * @author Georg Ehrke <[email protected]>
13
 * @author Jakob Sack <[email protected]>
14
 * @author Joas Schilling <[email protected]>
15
 * @author Jörn Friedrich Dreyer <[email protected]>
16
 * @author Kamil Domanski <[email protected]>
17
 * @author Lukas Reschke <[email protected]>
18
 * @author michag86 <[email protected]>
19
 * @author Morris Jobke <[email protected]>
20
 * @author Robin Appelman <[email protected]>
21
 * @author Roeland Jago Douma <[email protected]>
22
 * @author root <root@oc.(none)>
23
 * @author Thomas Müller <[email protected]>
24
 * @author Thomas Tanghus <[email protected]>
25
 *
26
 * @license AGPL-3.0
27
 *
28
 * This code is free software: you can redistribute it and/or modify
29
 * it under the terms of the GNU Affero General Public License, version 3,
30
 * as published by the Free Software Foundation.
31
 *
32
 * This program is distributed in the hope that it will be useful,
33
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
34
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
35
 * GNU Affero General Public License for more details.
36
 *
37
 * You should have received a copy of the GNU Affero General Public License, version 3,
38
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
39
 *
40
 */
41
42
namespace OC;
43
44
use Doctrine\DBAL\Exception\TableExistsException;
45
use OC\App\AppManager;
46
use OC\App\AppStore\Bundles\Bundle;
47
use OC\App\AppStore\Fetcher\AppFetcher;
48
use OC\App\CodeChecker\CodeChecker;
49
use OC\App\CodeChecker\EmptyCheck;
50
use OC\App\CodeChecker\PrivateCheck;
51
use OC\Archive\TAR;
52
use OC_App;
53
use OC_DB;
54
use OC_Helper;
55
use OCP\App\IAppManager;
56
use OCP\Http\Client\IClientService;
57
use OCP\IConfig;
58
use OCP\ILogger;
59
use OCP\ITempManager;
60
use phpseclib\File\X509;
61
62
/**
63
 * This class provides the functionality needed to install, update and remove apps
64
 */
65
class Installer {
66
	/** @var AppFetcher */
67
	private $appFetcher;
68
	/** @var IClientService */
69
	private $clientService;
70
	/** @var ITempManager */
71
	private $tempManager;
72
	/** @var ILogger */
73
	private $logger;
74
	/** @var IConfig */
75
	private $config;
76
77
	/**
78
	 * @param AppFetcher $appFetcher
79
	 * @param IClientService $clientService
80
	 * @param ITempManager $tempManager
81
	 * @param ILogger $logger
82
	 * @param IConfig $config
83
	 */
84
	public function __construct(AppFetcher $appFetcher,
85
								IClientService $clientService,
86
								ITempManager $tempManager,
87
								ILogger $logger,
88
								IConfig $config) {
89
		$this->appFetcher = $appFetcher;
90
		$this->clientService = $clientService;
91
		$this->tempManager = $tempManager;
92
		$this->logger = $logger;
93
		$this->config = $config;
94
	}
95
96
	/**
97
	 * Installs an app that is located in one of the app folders already
98
	 *
99
	 * @param string $appId App to install
100
	 * @throws \Exception
101
	 * @return integer
102
	 */
103
	public function installApp($appId) {
104
		$app = \OC_App::findAppInDirectories($appId);
105
		if($app === false) {
106
			throw new \Exception('App not found in any app directory');
107
		}
108
109
		$basedir = $app['path'].'/'.$appId;
110
		$info = OC_App::getAppInfo($basedir.'/appinfo/info.xml', true);
111
112
		//install the database
113
		if(is_file($basedir.'/appinfo/database.xml')) {
114
			if (\OC::$server->getAppConfig()->getValue($info['id'], 'installed_version') === null) {
0 ignored issues
show
Deprecated Code introduced by
The method OCP\IAppConfig::getValue() has been deprecated with message: 8.0.0 use method getAppValue of \OCP\IConfig This function gets a value from the appconfig table. If the key does
not exist the default value will be returned

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
115
				OC_DB::createDbFromStructure($basedir.'/appinfo/database.xml');
116
			} else {
117
				OC_DB::updateDbFromStructure($basedir.'/appinfo/database.xml');
118
			}
119
		}
120
121
		\OC_App::registerAutoloading($appId, $basedir);
122
		\OC_App::setupBackgroundJobs($info['background-jobs']);
123
124
		//run appinfo/install.php
125
		if((!isset($data['noinstall']) or $data['noinstall']==false)) {
0 ignored issues
show
Bug introduced by
The variable $data seems to never exist, and therefore isset should always return false. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
126
			self::includeAppScript($basedir . '/appinfo/install.php');
127
		}
128
129
		$appData = OC_App::getAppInfo($appId);
130
		OC_App::executeRepairSteps($appId, $appData['repair-steps']['install']);
131
132
		//set the installed version
133
		\OC::$server->getConfig()->setAppValue($info['id'], 'installed_version', OC_App::getAppVersion($info['id'], false));
134
		\OC::$server->getConfig()->setAppValue($info['id'], 'enabled', 'no');
135
136
		//set remote/public handlers
137
		foreach($info['remote'] as $name=>$path) {
138
			\OC::$server->getConfig()->setAppValue('core', 'remote_'.$name, $info['id'].'/'.$path);
139
		}
140 View Code Duplication
		foreach($info['public'] as $name=>$path) {
141
			\OC::$server->getConfig()->setAppValue('core', 'public_'.$name, $info['id'].'/'.$path);
142
		}
143
144
		OC_App::setAppTypes($info['id']);
145
146
		return $info['id'];
147
	}
148
149
	/**
150
	 * @brief checks whether or not an app is installed
151
	 * @param string $app app
152
	 * @returns bool
153
	 *
154
	 * Checks whether or not an app is installed, i.e. registered in apps table.
155
	 */
156
	public static function isInstalled( $app ) {
157
		return (\OC::$server->getConfig()->getAppValue($app, "installed_version", null) !== null);
158
	}
159
160
	/**
161
	 * Updates the specified app from the appstore
162
	 *
163
	 * @param string $appId
164
	 * @return bool
165
	 */
166
	public function updateAppstoreApp($appId) {
167
		if(self::isUpdateAvailable($appId, $this->appFetcher)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression self::isUpdateAvailable(...pId, $this->appFetcher) of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
168
			try {
169
				$this->downloadApp($appId);
170
			} catch (\Exception $e) {
171
				$this->logger->error($e->getMessage(), ['app' => 'core']);
172
				return false;
173
			}
174
			return OC_App::updateApp($appId);
175
		}
176
177
		return false;
178
	}
179
180
	/**
181
	 * Downloads an app and puts it into the app directory
182
	 *
183
	 * @param string $appId
184
	 *
185
	 * @throws \Exception If the installation was not successful
186
	 */
187
	public function downloadApp($appId) {
188
		$appId = strtolower($appId);
189
190
		$apps = $this->appFetcher->get();
191
		foreach($apps as $app) {
192
			if($app['id'] === $appId) {
193
				// Load the certificate
194
				$certificate = new X509();
195
				$certificate->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
196
				$loadedCertificate = $certificate->loadX509($app['certificate']);
197
198
				// Verify if the certificate has been revoked
199
				$crl = new X509();
200
				$crl->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
201
				$crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
202
				if($crl->validateSignature() !== true) {
203
					throw new \Exception('Could not validate CRL signature');
204
				}
205
				$csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString();
206
				$revoked = $crl->getRevoked($csn);
207
				if ($revoked !== false) {
208
					throw new \Exception(
209
						sprintf(
210
							'Certificate "%s" has been revoked',
211
							$csn
212
						)
213
					);
214
				}
215
216
				// Verify if the certificate has been issued by the Nextcloud Code Authority CA
217
				if($certificate->validateSignature() !== true) {
218
					throw new \Exception(
219
						sprintf(
220
							'App with id %s has a certificate not issued by a trusted Code Signing Authority',
221
							$appId
222
						)
223
					);
224
				}
225
226
				// Verify if the certificate is issued for the requested app id
227
				$certInfo = openssl_x509_parse($app['certificate']);
228
				if(!isset($certInfo['subject']['CN'])) {
229
					throw new \Exception(
230
						sprintf(
231
							'App with id %s has a cert with no CN',
232
							$appId
233
						)
234
					);
235
				}
236
				if($certInfo['subject']['CN'] !== $appId) {
237
					throw new \Exception(
238
						sprintf(
239
							'App with id %s has a cert issued to %s',
240
							$appId,
241
							$certInfo['subject']['CN']
242
						)
243
					);
244
				}
245
246
				// Download the release
247
				$tempFile = $this->tempManager->getTemporaryFile('.tar.gz');
248
				$client = $this->clientService->newClient();
249
				$client->get($app['releases'][0]['download'], ['save_to' => $tempFile]);
250
251
				// Check if the signature actually matches the downloaded content
252
				$certificate = openssl_get_publickey($app['certificate']);
253
				$verified = (bool)openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
254
				openssl_free_key($certificate);
255
256
				if($verified === true) {
257
					// Seems to match, let's proceed
258
					$extractDir = $this->tempManager->getTemporaryFolder();
259
					$archive = new TAR($tempFile);
260
261
					if($archive) {
262
						$archive->extract($extractDir);
263
						$allFiles = scandir($extractDir);
264
						$folders = array_diff($allFiles, ['.', '..']);
265
						$folders = array_values($folders);
266
267
						if(count($folders) > 1) {
268
							throw new \Exception(
269
								sprintf(
270
									'Extracted app %s has more than 1 folder',
271
									$appId
272
								)
273
							);
274
						}
275
276
						// Check if appinfo/info.xml has the same app ID as well
277
						$loadEntities = libxml_disable_entity_loader(false);
278
						$xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
279
						libxml_disable_entity_loader($loadEntities);
280
						if((string)$xml->id !== $appId) {
281
							throw new \Exception(
282
								sprintf(
283
									'App for id %s has a wrong app ID in info.xml: %s',
284
									$appId,
285
									(string)$xml->id
286
								)
287
							);
288
						}
289
290
						// Check if the version is lower than before
291
						$currentVersion = OC_App::getAppVersion($appId);
292
						$newVersion = (string)$xml->version;
293
						if(version_compare($currentVersion, $newVersion) === 1) {
294
							throw new \Exception(
295
								sprintf(
296
									'App for id %s has version %s and tried to update to lower version %s',
297
									$appId,
298
									$currentVersion,
299
									$newVersion
300
								)
301
							);
302
						}
303
304
						$baseDir = OC_App::getInstallPath() . '/' . $appId;
305
						// Remove old app with the ID if existent
306
						OC_Helper::rmdirr($baseDir);
307
						// Move to app folder
308
						if(@mkdir($baseDir)) {
309
							$extractDir .= '/' . $folders[0];
310
							OC_Helper::copyr($extractDir, $baseDir);
311
						}
312
						OC_Helper::copyr($extractDir, $baseDir);
313
						OC_Helper::rmdirr($extractDir);
314
						return;
315
					} else {
316
						throw new \Exception(
317
							sprintf(
318
								'Could not extract app with ID %s to %s',
319
								$appId,
320
								$extractDir
321
							)
322
						);
323
					}
324
				} else {
325
					// Signature does not match
326
					throw new \Exception(
327
						sprintf(
328
							'App with id %s has invalid signature',
329
							$appId
330
						)
331
					);
332
				}
333
			}
334
		}
335
336
		throw new \Exception(
337
			sprintf(
338
				'Could not download app %s',
339
				$appId
340
			)
341
		);
342
	}
343
344
	/**
345
	 * Check if an update for the app is available
346
	 *
347
	 * @param string $appId
348
	 * @param AppFetcher $appFetcher
349
	 * @return string|false false or the version number of the update
350
	 */
351
	public static function isUpdateAvailable($appId,
352
									  AppFetcher $appFetcher) {
353
		static $isInstanceReadyForUpdates = null;
354
355
		if ($isInstanceReadyForUpdates === null) {
356
			$installPath = OC_App::getInstallPath();
357
			if ($installPath === false || $installPath === null) {
358
				$isInstanceReadyForUpdates = false;
359
			} else {
360
				$isInstanceReadyForUpdates = true;
361
			}
362
		}
363
364
		if ($isInstanceReadyForUpdates === false) {
365
			return false;
366
		}
367
368
		$apps = $appFetcher->get();
369
		foreach($apps as $app) {
370
			if($app['id'] === $appId) {
371
				$currentVersion = OC_App::getAppVersion($appId);
372
				$newestVersion = $app['releases'][0]['version'];
373
				if (version_compare($newestVersion, $currentVersion, '>')) {
374
					return $newestVersion;
375
				} else {
376
					return false;
377
				}
378
			}
379
		}
380
381
		return false;
382
	}
383
384
	/**
385
	 * Check if app is already downloaded
386
	 * @param string $name name of the application to remove
387
	 * @return boolean
388
	 *
389
	 * The function will check if the app is already downloaded in the apps repository
390
	 */
391
	public function isDownloaded($name) {
392
		foreach(\OC::$APPSROOTS as $dir) {
393
			$dirToTest  = $dir['path'];
394
			$dirToTest .= '/';
395
			$dirToTest .= $name;
396
			$dirToTest .= '/';
397
398
			if (is_dir($dirToTest)) {
399
				return true;
400
			}
401
		}
402
403
		return false;
404
	}
405
406
	/**
407
	 * Removes an app
408
	 * @param string $appId ID of the application to remove
409
	 * @return boolean
410
	 *
411
	 *
412
	 * This function works as follows
413
	 *   -# call uninstall repair steps
414
	 *   -# removing the files
415
	 *
416
	 * The function will not delete preferences, tables and the configuration,
417
	 * this has to be done by the function oc_app_uninstall().
418
	 */
419
	public function removeApp($appId) {
420
		if($this->isDownloaded( $appId )) {
421
			$appDir = OC_App::getInstallPath() . '/' . $appId;
422
			OC_Helper::rmdirr($appDir);
423
			return true;
424
		}else{
425
			\OCP\Util::writeLog('core', 'can\'t remove app '.$appId.'. It is not installed.', \OCP\Util::ERROR);
426
427
			return false;
428
		}
429
430
	}
431
432
	/**
433
	 * Installs the app within the bundle and marks the bundle as installed
434
	 *
435
	 * @param Bundle $bundle
436
	 * @throws \Exception If app could not get installed
437
	 */
438
	public function installAppBundle(Bundle $bundle) {
439
		$appIds = $bundle->getAppIdentifiers();
440
		foreach($appIds as $appId) {
441
			if(!$this->isDownloaded($appId)) {
442
				$this->downloadApp($appId);
443
			}
444
			$this->installApp($appId);
445
			$app = new OC_App();
446
			$app->enable($appId);
447
		}
448
		$bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true);
449
		$bundles[] = $bundle->getIdentifier();
450
		$this->config->setAppValue('core', 'installed.bundles', json_encode($bundles));
451
	}
452
453
	/**
454
	 * Installs shipped apps
455
	 *
456
	 * This function installs all apps found in the 'apps' directory that should be enabled by default;
457
	 * @param bool $softErrors When updating we ignore errors and simply log them, better to have a
458
	 *                         working ownCloud at the end instead of an aborted update.
459
	 * @return array Array of error messages (appid => Exception)
460
	 */
461
	public static function installShippedApps($softErrors = false) {
462
		$errors = [];
463
		foreach(\OC::$APPSROOTS as $app_dir) {
464
			if($dir = opendir( $app_dir['path'] )) {
465
				while( false !== ( $filename = readdir( $dir ))) {
466
					if( substr( $filename, 0, 1 ) != '.' and is_dir($app_dir['path']."/$filename") ) {
467
						if( file_exists( $app_dir['path']."/$filename/appinfo/info.xml" )) {
468
							if(!Installer::isInstalled($filename)) {
469
								$info=OC_App::getAppInfo($filename);
470
								$enabled = isset($info['default_enable']);
471
								if (($enabled || in_array($filename, \OC::$server->getAppManager()->getAlwaysEnabledApps()))
472
									  && \OC::$server->getConfig()->getAppValue($filename, 'enabled') !== 'no') {
473
									if ($softErrors) {
474
										try {
475
											Installer::installShippedApp($filename);
476
										} catch (HintException $e) {
477
											if ($e->getPrevious() instanceof TableExistsException) {
0 ignored issues
show
Bug introduced by
The class Doctrine\DBAL\Exception\TableExistsException does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
478
												$errors[$filename] = $e;
479
												continue;
480
											}
481
											throw $e;
482
										}
483
									} else {
484
										Installer::installShippedApp($filename);
485
									}
486
									\OC::$server->getConfig()->setAppValue($filename, 'enabled', 'yes');
487
								}
488
							}
489
						}
490
					}
491
				}
492
				closedir( $dir );
493
			}
494
		}
495
496
		return $errors;
497
	}
498
499
	/**
500
	 * install an app already placed in the app folder
501
	 * @param string $app id of the app to install
502
	 * @return integer
503
	 */
504
	public static function installShippedApp($app) {
505
		//install the database
506
		$appPath = OC_App::getAppPath($app);
507
		if(is_file("$appPath/appinfo/database.xml")) {
508
			try {
509
				OC_DB::createDbFromStructure("$appPath/appinfo/database.xml");
510
			} catch (TableExistsException $e) {
0 ignored issues
show
Bug introduced by
The class Doctrine\DBAL\Exception\TableExistsException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
511
				throw new HintException(
512
					'Failed to enable app ' . $app,
513
					'Please ask for help via one of our <a href="https://nextcloud.com/support/" target="_blank" rel="noreferrer">support channels</a>.',
514
					0, $e
515
				);
516
			}
517
		}
518
519
		//run appinfo/install.php
520
		\OC_App::registerAutoloading($app, $appPath);
0 ignored issues
show
Security Bug introduced by
It seems like $appPath defined by \OC_App::getAppPath($app) on line 506 can also be of type false; however, OC_App::registerAutoloading() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
521
		self::includeAppScript("$appPath/appinfo/install.php");
522
523
		$info = OC_App::getAppInfo($app);
524
		if (is_null($info)) {
525
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by OC\Installer::installShippedApp of type integer.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
526
		}
527
		\OC_App::setupBackgroundJobs($info['background-jobs']);
528
529
		OC_App::executeRepairSteps($app, $info['repair-steps']['install']);
530
531
		$config = \OC::$server->getConfig();
532
533
		$config->setAppValue($app, 'installed_version', OC_App::getAppVersion($app));
534
		if (array_key_exists('ocsid', $info)) {
535
			$config->setAppValue($app, 'ocsid', $info['ocsid']);
536
		}
537
538
		//set remote/public handlers
539
		foreach($info['remote'] as $name=>$path) {
540
			$config->setAppValue('core', 'remote_'.$name, $app.'/'.$path);
541
		}
542
		foreach($info['public'] as $name=>$path) {
543
			$config->setAppValue('core', 'public_'.$name, $app.'/'.$path);
544
		}
545
546
		OC_App::setAppTypes($info['id']);
547
548
		if(isset($info['settings']) && is_array($info['settings'])) {
549
			// requires that autoloading was registered for the app,
550
			// as happens before running the install.php some lines above
551
			\OC::$server->getSettingsManager()->setupSettings($info['settings']);
552
		}
553
554
		return $info['id'];
555
	}
556
557
	/**
558
	 * check the code of an app with some static code checks
559
	 * @param string $folder the folder of the app to check
560
	 * @return boolean true for app is o.k. and false for app is not o.k.
561
	 */
562
	public static function checkCode($folder) {
563
		// is the code checker enabled?
564
		if(!\OC::$server->getConfig()->getSystemValue('appcodechecker', false)) {
565
			return true;
566
		}
567
568
		$codeChecker = new CodeChecker(new PrivateCheck(new EmptyCheck()));
569
		$errors = $codeChecker->analyseFolder(basename($folder), $folder);
570
571
		return empty($errors);
572
	}
573
574
	/**
575
	 * @param string $script
576
	 */
577
	private static function includeAppScript($script) {
578
		if ( file_exists($script) ){
579
			include $script;
580
		}
581
	}
582
}
583