diff options
author | Roeland Jago Douma <rullzer@owncloud.com> | 2016-04-13 19:53:05 +0200 |
---|---|---|
committer | Roeland Jago Douma <rullzer@owncloud.com> | 2016-04-13 19:53:05 +0200 |
commit | e5dd4272d3b5cef872cacf3cb09a5875bf0c420c (patch) | |
tree | 9fd126aa67d560aff1aa30b41773729d6377b966 /lib/private/App | |
parent | 3c0a1d4241c16c13b3fd93406402320284d153d9 (diff) | |
download | nextcloud-server-e5dd4272d3b5cef872cacf3cb09a5875bf0c420c.tar.gz nextcloud-server-e5dd4272d3b5cef872cacf3cb09a5875bf0c420c.zip |
Move \OC\App to PSR-4
Diffstat (limited to 'lib/private/App')
-rw-r--r-- | lib/private/App/AppManager.php | 370 | ||||
-rw-r--r-- | lib/private/App/CodeChecker/AbstractCheck.php | 139 | ||||
-rw-r--r-- | lib/private/App/CodeChecker/CodeChecker.php | 125 | ||||
-rw-r--r-- | lib/private/App/CodeChecker/DeprecationCheck.php | 168 | ||||
-rw-r--r-- | lib/private/App/CodeChecker/EmptyCheck.php | 67 | ||||
-rw-r--r-- | lib/private/App/CodeChecker/ICheck.php | 55 | ||||
-rw-r--r-- | lib/private/App/CodeChecker/InfoChecker.php | 160 | ||||
-rw-r--r-- | lib/private/App/CodeChecker/NodeVisitor.php | 307 | ||||
-rw-r--r-- | lib/private/App/CodeChecker/PrivateCheck.php | 84 | ||||
-rw-r--r-- | lib/private/App/CodeChecker/StrongComparisonCheck.php | 78 | ||||
-rw-r--r-- | lib/private/App/DependencyAnalyzer.php | 319 | ||||
-rw-r--r-- | lib/private/App/InfoParser.php | 164 | ||||
-rw-r--r-- | lib/private/App/Platform.php | 92 | ||||
-rw-r--r-- | lib/private/App/PlatformRepository.php | 228 |
14 files changed, 2356 insertions, 0 deletions
diff --git a/lib/private/App/AppManager.php b/lib/private/App/AppManager.php new file mode 100644 index 00000000000..69e5334774e --- /dev/null +++ b/lib/private/App/AppManager.php @@ -0,0 +1,370 @@ +<?php +/** + * @author Arthur Schiwon <blizzz@owncloud.com> + * @author Christoph Schaefer <christophł@wolkesicher.de> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\App; + +use OCP\App\IAppManager; +use OCP\App\ManagerEvent; +use OCP\IAppConfig; +use OCP\ICacheFactory; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserSession; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; + +class AppManager implements IAppManager { + + /** + * Apps with these types can not be enabled for certain groups only + * @var string[] + */ + protected $protectedAppTypes = [ + 'filesystem', + 'prelogin', + 'authentication', + 'logging', + 'prevent_group_restriction', + ]; + + /** @var \OCP\IUserSession */ + private $userSession; + + /** @var \OCP\IAppConfig */ + private $appConfig; + + /** @var \OCP\IGroupManager */ + private $groupManager; + + /** @var \OCP\ICacheFactory */ + private $memCacheFactory; + + /** @var string[] $appId => $enabled */ + private $installedAppsCache; + + /** @var string[] */ + private $shippedApps; + + /** @var string[] */ + private $alwaysEnabled; + + /** @var EventDispatcherInterface */ + private $dispatcher; + + /** + * @param \OCP\IUserSession $userSession + * @param \OCP\IAppConfig $appConfig + * @param \OCP\IGroupManager $groupManager + * @param \OCP\ICacheFactory $memCacheFactory + */ + public function __construct(IUserSession $userSession, + IAppConfig $appConfig, + IGroupManager $groupManager, + ICacheFactory $memCacheFactory, + EventDispatcherInterface $dispatcher) { + $this->userSession = $userSession; + $this->appConfig = $appConfig; + $this->groupManager = $groupManager; + $this->memCacheFactory = $memCacheFactory; + $this->dispatcher = $dispatcher; + } + + /** + * @return string[] $appId => $enabled + */ + private function getInstalledAppsValues() { + if (!$this->installedAppsCache) { + $values = $this->appConfig->getValues(false, 'enabled'); + + $alwaysEnabledApps = $this->getAlwaysEnabledApps(); + foreach($alwaysEnabledApps as $appId) { + $values[$appId] = 'yes'; + } + + $this->installedAppsCache = array_filter($values, function ($value) { + return $value !== 'no'; + }); + ksort($this->installedAppsCache); + } + return $this->installedAppsCache; + } + + /** + * List all installed apps + * + * @return string[] + */ + public function getInstalledApps() { + return array_keys($this->getInstalledAppsValues()); + } + + /** + * List all apps enabled for a user + * + * @param \OCP\IUser $user + * @return string[] + */ + public function getEnabledAppsForUser(IUser $user) { + $apps = $this->getInstalledAppsValues(); + $appsForUser = array_filter($apps, function ($enabled) use ($user) { + return $this->checkAppForUser($enabled, $user); + }); + return array_keys($appsForUser); + } + + /** + * Check if an app is enabled for user + * + * @param string $appId + * @param \OCP\IUser $user (optional) if not defined, the currently logged in user will be used + * @return bool + */ + public function isEnabledForUser($appId, $user = null) { + if ($this->isAlwaysEnabled($appId)) { + return true; + } + if (is_null($user)) { + $user = $this->userSession->getUser(); + } + $installedApps = $this->getInstalledAppsValues(); + if (isset($installedApps[$appId])) { + return $this->checkAppForUser($installedApps[$appId], $user); + } else { + return false; + } + } + + /** + * @param string $enabled + * @param IUser $user + * @return bool + */ + private function checkAppForUser($enabled, $user) { + if ($enabled === 'yes') { + return true; + } elseif (is_null($user)) { + return false; + } else { + if(empty($enabled)){ + return false; + } + + $groupIds = json_decode($enabled); + + if (!is_array($groupIds)) { + $jsonError = json_last_error(); + \OC::$server->getLogger()->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . print_r($enabled, true) . ' - json error code: ' . $jsonError, ['app' => 'lib']); + return false; + } + + $userGroups = $this->groupManager->getUserGroupIds($user); + foreach ($userGroups as $groupId) { + if (array_search($groupId, $groupIds) !== false) { + return true; + } + } + return false; + } + } + + /** + * Check if an app is installed in the instance + * + * @param string $appId + * @return bool + */ + public function isInstalled($appId) { + $installedApps = $this->getInstalledAppsValues(); + return isset($installedApps[$appId]); + } + + /** + * Enable an app for every user + * + * @param string $appId + */ + public function enableApp($appId) { + $this->installedAppsCache[$appId] = 'yes'; + $this->appConfig->setValue($appId, 'enabled', 'yes'); + $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent( + ManagerEvent::EVENT_APP_ENABLE, $appId + )); + $this->clearAppsCache(); + } + + /** + * Enable an app only for specific groups + * + * @param string $appId + * @param \OCP\IGroup[] $groups + * @throws \Exception if app can't be enabled for groups + */ + public function enableAppForGroups($appId, $groups) { + $info = $this->getAppInfo($appId); + if (!empty($info['types'])) { + $protectedTypes = array_intersect($this->protectedAppTypes, $info['types']); + if (!empty($protectedTypes)) { + throw new \Exception("$appId can't be enabled for groups."); + } + } + + $groupIds = array_map(function ($group) { + /** @var \OCP\IGroup $group */ + return $group->getGID(); + }, $groups); + $this->installedAppsCache[$appId] = json_encode($groupIds); + $this->appConfig->setValue($appId, 'enabled', json_encode($groupIds)); + $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent( + ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups + )); + $this->clearAppsCache(); + } + + /** + * Disable an app for every user + * + * @param string $appId + * @throws \Exception if app can't be disabled + */ + public function disableApp($appId) { + if ($this->isAlwaysEnabled($appId)) { + throw new \Exception("$appId can't be disabled."); + } + unset($this->installedAppsCache[$appId]); + $this->appConfig->setValue($appId, 'enabled', 'no'); + $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent( + ManagerEvent::EVENT_APP_DISABLE, $appId + )); + $this->clearAppsCache(); + } + + /** + * Clear the cached list of apps when enabling/disabling an app + */ + public function clearAppsCache() { + $settingsMemCache = $this->memCacheFactory->create('settings'); + $settingsMemCache->clear('listApps'); + } + + /** + * Returns a list of apps that need upgrade + * + * @param array $version ownCloud version as array of version components + * @return array list of app info from apps that need an upgrade + * + * @internal + */ + public function getAppsNeedingUpgrade($ocVersion) { + $appsToUpgrade = []; + $apps = $this->getInstalledApps(); + foreach ($apps as $appId) { + $appInfo = $this->getAppInfo($appId); + $appDbVersion = $this->appConfig->getValue($appId, 'installed_version'); + if ($appDbVersion + && isset($appInfo['version']) + && version_compare($appInfo['version'], $appDbVersion, '>') + && \OC_App::isAppCompatible($ocVersion, $appInfo) + ) { + $appsToUpgrade[] = $appInfo; + } + } + + return $appsToUpgrade; + } + + /** + * Returns the app information from "appinfo/info.xml". + * + * @param string $appId app id + * + * @return array app iinfo + * + * @internal + */ + public function getAppInfo($appId) { + $appInfo = \OC_App::getAppInfo($appId); + if (!isset($appInfo['version'])) { + // read version from separate file + $appInfo['version'] = \OC_App::getAppVersion($appId); + } + return $appInfo; + } + + /** + * Returns a list of apps incompatible with the given version + * + * @param array $version ownCloud version as array of version components + * + * @return array list of app info from incompatible apps + * + * @internal + */ + public function getIncompatibleApps($version) { + $apps = $this->getInstalledApps(); + $incompatibleApps = array(); + foreach ($apps as $appId) { + $info = $this->getAppInfo($appId); + if (!\OC_App::isAppCompatible($version, $info)) { + $incompatibleApps[] = $info; + } + } + return $incompatibleApps; + } + + /** + * @inheritdoc + */ + public function isShipped($appId) { + $this->loadShippedJson(); + return in_array($appId, $this->shippedApps); + } + + private function isAlwaysEnabled($appId) { + $alwaysEnabled = $this->getAlwaysEnabledApps(); + return in_array($appId, $alwaysEnabled); + } + + private function loadShippedJson() { + if (is_null($this->shippedApps)) { + $shippedJson = \OC::$SERVERROOT . '/core/shipped.json'; + if (!file_exists($shippedJson)) { + throw new \Exception("File not found: $shippedJson"); + } + $content = json_decode(file_get_contents($shippedJson), true); + $this->shippedApps = $content['shippedApps']; + $this->alwaysEnabled = $content['alwaysEnabled']; + } + } + + /** + * @inheritdoc + */ + public function getAlwaysEnabledApps() { + $this->loadShippedJson(); + return $this->alwaysEnabled; + } +} diff --git a/lib/private/App/CodeChecker/AbstractCheck.php b/lib/private/App/CodeChecker/AbstractCheck.php new file mode 100644 index 00000000000..ca91d366482 --- /dev/null +++ b/lib/private/App/CodeChecker/AbstractCheck.php @@ -0,0 +1,139 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\App\CodeChecker; + +abstract class AbstractCheck implements ICheck { + /** @var ICheck */ + protected $check; + + /** + * @param ICheck $check + */ + public function __construct(ICheck $check) { + $this->check = $check; + } + + /** + * @param int $errorCode + * @param string $errorObject + * @return string + */ + public function getDescription($errorCode, $errorObject) { + switch ($errorCode) { + case CodeChecker::STATIC_CALL_NOT_ALLOWED: + $functions = $this->getLocalFunctions(); + $functions = array_change_key_case($functions, CASE_LOWER); + if (isset($functions[$errorObject])) { + return $this->getLocalDescription(); + } + // no break; + case CodeChecker::CLASS_EXTENDS_NOT_ALLOWED: + case CodeChecker::CLASS_IMPLEMENTS_NOT_ALLOWED: + case CodeChecker::CLASS_NEW_NOT_ALLOWED: + case CodeChecker::CLASS_USE_NOT_ALLOWED: + $classes = $this->getLocalClasses(); + $classes = array_change_key_case($classes, CASE_LOWER); + if (isset($classes[$errorObject])) { + return $this->getLocalDescription(); + } + break; + + case CodeChecker::CLASS_CONST_FETCH_NOT_ALLOWED: + $constants = $this->getLocalConstants(); + $constants = array_change_key_case($constants, CASE_LOWER); + if (isset($constants[$errorObject])) { + return $this->getLocalDescription(); + } + break; + + case CodeChecker::CLASS_METHOD_CALL_NOT_ALLOWED: + $methods = $this->getLocalMethods(); + $methods = array_change_key_case($methods, CASE_LOWER); + if (isset($methods[$errorObject])) { + return $this->getLocalDescription(); + } + break; + } + + return $this->check->getDescription($errorCode, $errorObject); + } + + /** + * @return string + */ + abstract protected function getLocalDescription(); + + /** + * @return array + */ + abstract protected function getLocalClasses(); + + /** + * @return array + */ + abstract protected function getLocalConstants(); + + /** + * @return array + */ + abstract protected function getLocalFunctions(); + + /** + * @return array + */ + abstract protected function getLocalMethods(); + + /** + * @return array E.g.: `'ClassName' => 'oc version',` + */ + public function getClasses() { + return array_merge($this->getLocalClasses(), $this->check->getClasses()); + } + + /** + * @return array E.g.: `'ClassName::CONSTANT_NAME' => 'oc version',` + */ + public function getConstants() { + return array_merge($this->getLocalConstants(), $this->check->getConstants()); + } + + /** + * @return array E.g.: `'functionName' => 'oc version',` + */ + public function getFunctions() { + return array_merge($this->getLocalFunctions(), $this->check->getFunctions()); + } + + /** + * @return array E.g.: `'ClassName::methodName' => 'oc version',` + */ + public function getMethods() { + return array_merge($this->getLocalMethods(), $this->check->getMethods()); + } + + /** + * @return bool + */ + public function checkStrongComparisons() { + return $this->check->checkStrongComparisons(); + } +} diff --git a/lib/private/App/CodeChecker/CodeChecker.php b/lib/private/App/CodeChecker/CodeChecker.php new file mode 100644 index 00000000000..0ca597ccb4e --- /dev/null +++ b/lib/private/App/CodeChecker/CodeChecker.php @@ -0,0 +1,125 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\App\CodeChecker; + +use OC\Hooks\BasicEmitter; +use PhpParser\Lexer; +use PhpParser\Node; +use PhpParser\Node\Name; +use PhpParser\NodeTraverser; +use PhpParser\Parser; +use RecursiveCallbackFilterIterator; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use RegexIterator; +use SplFileInfo; + +class CodeChecker extends BasicEmitter { + + const CLASS_EXTENDS_NOT_ALLOWED = 1000; + const CLASS_IMPLEMENTS_NOT_ALLOWED = 1001; + const STATIC_CALL_NOT_ALLOWED = 1002; + const CLASS_CONST_FETCH_NOT_ALLOWED = 1003; + const CLASS_NEW_NOT_ALLOWED = 1004; + const OP_OPERATOR_USAGE_DISCOURAGED = 1005; + const CLASS_USE_NOT_ALLOWED = 1006; + const CLASS_METHOD_CALL_NOT_ALLOWED = 1007; + + /** @var Parser */ + private $parser; + + /** @var ICheck */ + protected $checkList; + + public function __construct(ICheck $checkList) { + $this->checkList = $checkList; + $this->parser = new Parser(new Lexer); + } + + /** + * @param string $appId + * @return array + */ + public function analyse($appId) { + $appPath = \OC_App::getAppPath($appId); + if ($appPath === false) { + throw new \RuntimeException("No app with given id <$appId> known."); + } + + return $this->analyseFolder($appPath); + } + + /** + * @param string $folder + * @return array + */ + public function analyseFolder($folder) { + $errors = []; + + $excludes = array_map(function($item) use ($folder) { + return $folder . '/' . $item; + }, ['vendor', '3rdparty', '.git', 'l10n', 'tests', 'test']); + + $iterator = new RecursiveDirectoryIterator($folder, RecursiveDirectoryIterator::SKIP_DOTS); + $iterator = new RecursiveCallbackFilterIterator($iterator, function($item) use ($folder, $excludes){ + /** @var SplFileInfo $item */ + foreach($excludes as $exclude) { + if (substr($item->getPath(), 0, strlen($exclude)) === $exclude) { + return false; + } + } + return true; + }); + $iterator = new RecursiveIteratorIterator($iterator); + $iterator = new RegexIterator($iterator, '/^.+\.php$/i'); + + foreach ($iterator as $file) { + /** @var SplFileInfo $file */ + $this->emit('CodeChecker', 'analyseFileBegin', [$file->getPathname()]); + $fileErrors = $this->analyseFile($file); + $this->emit('CodeChecker', 'analyseFileFinished', [$file->getPathname(), $fileErrors]); + $errors = array_merge($fileErrors, $errors); + } + + return $errors; + } + + + /** + * @param string $file + * @return array + */ + public function analyseFile($file) { + $code = file_get_contents($file); + $statements = $this->parser->parse($code); + + $visitor = new NodeVisitor($this->checkList); + $traverser = new NodeTraverser; + $traverser->addVisitor($visitor); + + $traverser->traverse($statements); + + return $visitor->errors; + } +} diff --git a/lib/private/App/CodeChecker/DeprecationCheck.php b/lib/private/App/CodeChecker/DeprecationCheck.php new file mode 100644 index 00000000000..fa5eae8ab1d --- /dev/null +++ b/lib/private/App/CodeChecker/DeprecationCheck.php @@ -0,0 +1,168 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\App\CodeChecker; + +class DeprecationCheck extends AbstractCheck implements ICheck { + /** + * @return string + */ + protected function getLocalDescription() { + return 'deprecated'; + } + + /** + * @return array E.g.: `'ClassName' => 'oc version',` + */ + protected function getLocalClasses() { + return [ + 'OC_JSON' => '8.2.0', + + 'OCP\Config' => '8.0.0', + 'OCP\Contacts' => '8.1.0', + 'OCP\DB' => '8.1.0', + 'OCP\IHelper' => '8.1.0', + 'OCP\JSON' => '8.1.0', + 'OCP\Response' => '8.1.0', + 'OCP\AppFramework\IApi' => '8.0.0', + ]; + } + + /** + * @return array E.g.: `'ClassName::CONSTANT_NAME' => 'oc version',` + */ + protected function getLocalConstants() { + return [ + 'OC_API::GUEST_AUTH' => '8.2.0', + 'OC_API::USER_AUTH' => '8.2.0', + 'OC_API::SUBADMIN_AUTH' => '8.2.0', + 'OC_API::ADMIN_AUTH' => '8.2.0', + 'OC_API::RESPOND_UNAUTHORISED' => '8.2.0', + 'OC_API::RESPOND_SERVER_ERROR' => '8.2.0', + 'OC_API::RESPOND_NOT_FOUND' => '8.2.0', + 'OC_API::RESPOND_UNKNOWN_ERROR' => '8.2.0', + + 'OCP::PERMISSION_CREATE' => '8.0.0', + 'OCP::PERMISSION_READ' => '8.0.0', + 'OCP::PERMISSION_UPDATE' => '8.0.0', + 'OCP::PERMISSION_DELETE' => '8.0.0', + 'OCP::PERMISSION_SHARE' => '8.0.0', + 'OCP::PERMISSION_ALL' => '8.0.0', + 'OCP::FILENAME_INVALID_CHARS' => '8.0.0', + ]; + } + + /** + * @return array E.g.: `'functionName' => 'oc version',` + */ + protected function getLocalFunctions() { + return [ + 'OCP::image_path' => '8.0.0', + 'OCP::mimetype_icon' => '8.0.0', + 'OCP::preview_icon' => '8.0.0', + 'OCP::publicPreview_icon' => '8.0.0', + 'OCP::human_file_size' => '8.0.0', + 'OCP::relative_modified_date' => '8.0.0', + 'OCP::simple_file_size' => '8.0.0', + 'OCP::html_select_options' => '8.0.0', + ]; + } + + /** + * @return array E.g.: `'ClassName::methodName' => 'oc version',` + */ + protected function getLocalMethods() { + return [ + 'OC_L10N::get' => '8.2.0', + + 'OCP\Activity\IManager::publishActivity' => '8.2.0', + + 'OCP\App::register' => '8.1.0', + 'OCP\App::addNavigationEntry' => '8.1.0', + 'OCP\App::getActiveNavigationEntry' => '8.2.0', + 'OCP\App::setActiveNavigationEntry' => '8.1.0', + + 'OCP\AppFramework\Controller::params' => '7.0.0', + 'OCP\AppFramework\Controller::getParams' => '7.0.0', + 'OCP\AppFramework\Controller::method' => '7.0.0', + 'OCP\AppFramework\Controller::getUploadedFile' => '7.0.0', + 'OCP\AppFramework\Controller::env' => '7.0.0', + 'OCP\AppFramework\Controller::cookie' => '7.0.0', + 'OCP\AppFramework\Controller::render' => '7.0.0', + + 'OCP\AppFramework\IAppContainer::getCoreApi' => '8.0.0', + 'OCP\AppFramework\IAppContainer::isLoggedIn' => '8.0.0', + 'OCP\AppFramework\IAppContainer::isAdminUser' => '8.0.0', + 'OCP\AppFramework\IAppContainer::log' => '8.0.0', + + 'OCP\BackgroundJob::addQueuedTask' => '6.0.0', + 'OCP\BackgroundJob::addRegularTask' => '6.0.0', + 'OCP\BackgroundJob::allQueuedTasks' => '6.0.0', + 'OCP\BackgroundJob::allRegularTasks' => '6.0.0', + 'OCP\BackgroundJob::deleteQueuedTask' => '6.0.0', + 'OCP\BackgroundJob::findQueuedTask' => '6.0.0', + 'OCP\BackgroundJob::queuedTaskWhereAppIs' => '6.0.0', + 'OCP\BackgroundJob::registerJob' => '8.1.0', + + 'OCP\Files::tmpFile' => '8.1.0', + 'OCP\Files::tmpFolder' => '8.1.0', + + 'OCP\IAppConfig::getValue' => '8.0.0', + 'OCP\IAppConfig::deleteKey' => '8.0.0', + 'OCP\IAppConfig::getKeys' => '8.0.0', + 'OCP\IAppConfig::setValue' => '8.0.0', + 'OCP\IAppConfig::deleteApp' => '8.0.0', + + 'OCP\IDBConnection::createQueryBuilder' => '8.2.0', + 'OCP\IDBConnection::getExpressionBuilder' => '8.2.0', + + 'OCP\ISearch::search' => '8.0.0', + + 'OCP\IServerContainer::getCache' => '8.2.0', + 'OCP\IServerContainer::getDb' => '8.1.0', + 'OCP\IServerContainer::getHTTPHelper' => '8.1.0', + + 'OCP\User::getUser' => '8.0.0', + 'OCP\User::getUsers' => '8.1.0', + 'OCP\User::getDisplayName' => '8.1.0', + 'OCP\User::getDisplayNames' => '8.1.0', + 'OCP\User::userExists' => '8.1.0', + 'OCP\User::logout' => '8.1.0', + 'OCP\User::checkPassword' => '8.1.0', + + 'OCP\Util::encryptedFiles' => '8.1.0', + 'OCP\Util::formatDate' => '8.0.0', + 'OCP\Util::generateRandomBytes' => '8.1.0', + 'OCP\Util::getServerHost' => '8.1.0', + 'OCP\Util::getServerProtocol' => '8.1.0', + 'OCP\Util::getRequestUri' => '8.1.0', + 'OCP\Util::getScriptName' => '8.1.0', + 'OCP\Util::imagePath' => '8.1.0', + 'OCP\Util::isValidFileName' => '8.1.0', + 'OCP\Util::linkToRoute' => '8.1.0', + 'OCP\Util::linkTo' => '8.1.0', + 'OCP\Util::logException' => '8.2.0', + 'OCP\Util::mb_str_replace' => '8.2.0', + 'OCP\Util::mb_substr_replace' => '8.2.0', + 'OCP\Util::sendMail' => '8.1.0', + ]; + } +} diff --git a/lib/private/App/CodeChecker/EmptyCheck.php b/lib/private/App/CodeChecker/EmptyCheck.php new file mode 100644 index 00000000000..b779926d5e4 --- /dev/null +++ b/lib/private/App/CodeChecker/EmptyCheck.php @@ -0,0 +1,67 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +namespace OC\App\CodeChecker; + +class EmptyCheck implements ICheck { + /** + * @param int $errorCode + * @param string $errorObject + * @return string + */ + public function getDescription($errorCode, $errorObject) { + return ''; + } + + /** + * @return array E.g.: `'ClassName' => 'oc version',` + */ + public function getClasses() { + return []; + } + + /** + * @return array E.g.: `'ClassName::CONSTANT_NAME' => 'oc version',` + */ + public function getConstants() { + return []; + } + + /** + * @return array E.g.: `'functionName' => 'oc version',` + */ + public function getFunctions() { + return []; + } + + /** + * @return array E.g.: `'ClassName::methodName' => 'oc version',` + */ + public function getMethods() { + return []; + } + + /** + * @return bool + */ + public function checkStrongComparisons() { + return false; + } +} diff --git a/lib/private/App/CodeChecker/ICheck.php b/lib/private/App/CodeChecker/ICheck.php new file mode 100644 index 00000000000..97e0bc9b8a0 --- /dev/null +++ b/lib/private/App/CodeChecker/ICheck.php @@ -0,0 +1,55 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +namespace OC\App\CodeChecker; + +interface ICheck { + /** + * @param int $errorCode + * @param string $errorObject + * @return string + */ + public function getDescription($errorCode, $errorObject); + + /** + * @return array E.g.: `'ClassName' => 'oc version',` + */ + public function getClasses(); + + /** + * @return array E.g.: `'ClassName::CONSTANT_NAME' => 'oc version',` + */ + public function getConstants(); + + /** + * @return array E.g.: `'functionName' => 'oc version',` + */ + public function getFunctions(); + + /** + * @return array E.g.: `'ClassName::methodName' => 'oc version',` + */ + public function getMethods(); + + /** + * @return bool + */ + public function checkStrongComparisons(); +} diff --git a/lib/private/App/CodeChecker/InfoChecker.php b/lib/private/App/CodeChecker/InfoChecker.php new file mode 100644 index 00000000000..812007d8839 --- /dev/null +++ b/lib/private/App/CodeChecker/InfoChecker.php @@ -0,0 +1,160 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\App\CodeChecker; + +use OC\App\InfoParser; +use OC\Hooks\BasicEmitter; + +class InfoChecker extends BasicEmitter { + + /** @var InfoParser */ + private $infoParser; + + private $mandatoryFields = [ + 'author', + 'description', + 'id', + 'licence', + 'name', + ]; + private $optionalFields = [ + 'bugs', + 'category', + 'default_enable', + 'dependencies', // TODO: Mandatory as of ownCloud 11 + 'documentation', + 'namespace', + 'ocsid', + 'public', + 'remote', + 'repository', + 'types', + 'version', + 'website', + ]; + private $deprecatedFields = [ + 'info', + 'require', + 'requiremax', + 'requiremin', + 'shipped', + 'standalone', + ]; + + public function __construct(InfoParser $infoParser) { + $this->infoParser = $infoParser; + } + + /** + * @param string $appId + * @return array + */ + public function analyse($appId) { + $appPath = \OC_App::getAppPath($appId); + if ($appPath === false) { + throw new \RuntimeException("No app with given id <$appId> known."); + } + + $errors = []; + + $info = $this->infoParser->parse($appPath . '/appinfo/info.xml'); + + if (isset($info['dependencies']['owncloud']['@attributes']['min-version']) && ($info['requiremin'] || $info['require'])) { + $this->emit('InfoChecker', 'duplicateRequirement', ['min']); + $errors[] = [ + 'type' => 'duplicateRequirement', + 'field' => 'min', + ]; + } else if (!isset($info['dependencies']['owncloud']['@attributes']['min-version'])) { + $this->emit('InfoChecker', 'missingRequirement', ['min']); + } + + if (isset($info['dependencies']['owncloud']['@attributes']['max-version']) && $info['requiremax']) { + $this->emit('InfoChecker', 'duplicateRequirement', ['max']); + $errors[] = [ + 'type' => 'duplicateRequirement', + 'field' => 'max', + ]; + } else if (!isset($info['dependencies']['owncloud']['@attributes']['max-version'])) { + $this->emit('InfoChecker', 'missingRequirement', ['max']); + } + + foreach ($info as $key => $value) { + if(is_array($value)) { + $value = json_encode($value); + } + if (in_array($key, $this->mandatoryFields)) { + $this->emit('InfoChecker', 'mandatoryFieldFound', [$key, $value]); + continue; + } + + if (in_array($key, $this->optionalFields)) { + $this->emit('InfoChecker', 'optionalFieldFound', [$key, $value]); + continue; + } + + if (in_array($key, $this->deprecatedFields)) { + // skip empty arrays - empty arrays for remote and public are always added + if($value === '[]' && in_array($key, ['public', 'remote', 'info'])) { + continue; + } + $this->emit('InfoChecker', 'deprecatedFieldFound', [$key, $value]); + continue; + } + + $this->emit('InfoChecker', 'unusedFieldFound', [$key, $value]); + } + + foreach ($this->mandatoryFields as $key) { + if(!isset($info[$key])) { + $this->emit('InfoChecker', 'mandatoryFieldMissing', [$key]); + $errors[] = [ + 'type' => 'mandatoryFieldMissing', + 'field' => $key, + ]; + } + } + + $versionFile = $appPath . '/appinfo/version'; + if (is_file($versionFile)) { + $version = trim(file_get_contents($versionFile)); + if (isset($info['version'])) { + if($info['version'] !== $version) { + $this->emit('InfoChecker', 'differentVersions', + [$version, $info['version']]); + $errors[] = [ + 'type' => 'differentVersions', + 'message' => 'appinfo/version: ' . $version . + ' - appinfo/info.xml: ' . $info['version'], + ]; + } else { + $this->emit('InfoChecker', 'sameVersions', [$versionFile]); + } + } else { + $this->emit('InfoChecker', 'migrateVersion', [$version]); + } + } + + return $errors; + } +} diff --git a/lib/private/App/CodeChecker/NodeVisitor.php b/lib/private/App/CodeChecker/NodeVisitor.php new file mode 100644 index 00000000000..f9386caeeae --- /dev/null +++ b/lib/private/App/CodeChecker/NodeVisitor.php @@ -0,0 +1,307 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\App\CodeChecker; + +use PhpParser\Node; +use PhpParser\Node\Name; +use PhpParser\NodeVisitorAbstract; + +class NodeVisitor extends NodeVisitorAbstract { + /** @var ICheck */ + protected $list; + + /** @var string */ + protected $blackListDescription; + /** @var string[] */ + protected $blackListedClassNames; + /** @var string[] */ + protected $blackListedConstants; + /** @var string[] */ + protected $blackListedFunctions; + /** @var string[] */ + protected $blackListedMethods; + /** @var bool */ + protected $checkEqualOperatorUsage; + /** @var string[] */ + protected $errorMessages; + + /** + * @param ICheck $list + */ + public function __construct(ICheck $list) { + $this->list = $list; + + $this->blackListedClassNames = []; + foreach ($list->getClasses() as $class => $blackListInfo) { + if (is_numeric($class) && is_string($blackListInfo)) { + $class = $blackListInfo; + $blackListInfo = null; + } + + $class = strtolower($class); + $this->blackListedClassNames[$class] = $class; + } + + $this->blackListedConstants = []; + foreach ($list->getConstants() as $constantName => $blackListInfo) { + $constantName = strtolower($constantName); + $this->blackListedConstants[$constantName] = $constantName; + } + + $this->blackListedFunctions = []; + foreach ($list->getFunctions() as $functionName => $blackListInfo) { + $functionName = strtolower($functionName); + $this->blackListedFunctions[$functionName] = $functionName; + } + + $this->blackListedMethods = []; + foreach ($list->getMethods() as $functionName => $blackListInfo) { + $functionName = strtolower($functionName); + $this->blackListedMethods[$functionName] = $functionName; + } + + $this->checkEqualOperatorUsage = $list->checkStrongComparisons(); + + $this->errorMessages = [ + CodeChecker::CLASS_EXTENDS_NOT_ALLOWED => "%s class must not be extended", + CodeChecker::CLASS_IMPLEMENTS_NOT_ALLOWED => "%s interface must not be implemented", + CodeChecker::STATIC_CALL_NOT_ALLOWED => "Static method of %s class must not be called", + CodeChecker::CLASS_CONST_FETCH_NOT_ALLOWED => "Constant of %s class must not not be fetched", + CodeChecker::CLASS_NEW_NOT_ALLOWED => "%s class must not be instantiated", + CodeChecker::CLASS_USE_NOT_ALLOWED => "%s class must not be imported with a use statement", + CodeChecker::CLASS_METHOD_CALL_NOT_ALLOWED => "Method of %s class must not be called", + + CodeChecker::OP_OPERATOR_USAGE_DISCOURAGED => "is discouraged", + ]; + } + + /** @var array */ + public $errors = []; + + public function enterNode(Node $node) { + if ($this->checkEqualOperatorUsage && $node instanceof Node\Expr\BinaryOp\Equal) { + $this->errors[]= [ + 'disallowedToken' => '==', + 'errorCode' => CodeChecker::OP_OPERATOR_USAGE_DISCOURAGED, + 'line' => $node->getLine(), + 'reason' => $this->buildReason('==', CodeChecker::OP_OPERATOR_USAGE_DISCOURAGED) + ]; + } + if ($this->checkEqualOperatorUsage && $node instanceof Node\Expr\BinaryOp\NotEqual) { + $this->errors[]= [ + 'disallowedToken' => '!=', + 'errorCode' => CodeChecker::OP_OPERATOR_USAGE_DISCOURAGED, + 'line' => $node->getLine(), + 'reason' => $this->buildReason('!=', CodeChecker::OP_OPERATOR_USAGE_DISCOURAGED) + ]; + } + if ($node instanceof Node\Stmt\Class_) { + if (!is_null($node->extends)) { + $this->checkBlackList($node->extends->toString(), CodeChecker::CLASS_EXTENDS_NOT_ALLOWED, $node); + } + foreach ($node->implements as $implements) { + $this->checkBlackList($implements->toString(), CodeChecker::CLASS_IMPLEMENTS_NOT_ALLOWED, $node); + } + } + if ($node instanceof Node\Expr\StaticCall) { + if (!is_null($node->class)) { + if ($node->class instanceof Name) { + $this->checkBlackList($node->class->toString(), CodeChecker::STATIC_CALL_NOT_ALLOWED, $node); + + $this->checkBlackListFunction($node->class->toString(), $node->name, $node); + $this->checkBlackListMethod($node->class->toString(), $node->name, $node); + } + + if ($node->class instanceof Node\Expr\Variable) { + /** + * TODO: find a way to detect something like this: + * $c = "OC_API"; + * $n = $c::call(); + */ + // $this->checkBlackListMethod($node->class->..., $node->name, $node); + } + } + } + if ($node instanceof Node\Expr\MethodCall) { + if (!is_null($node->var)) { + if ($node->var instanceof Node\Expr\Variable) { + /** + * TODO: find a way to detect something like this: + * $c = new OC_API(); + * $n = $c::call(); + * $n = $c->call(); + */ + // $this->checkBlackListMethod($node->var->..., $node->name, $node); + } + } + } + if ($node instanceof Node\Expr\ClassConstFetch) { + if (!is_null($node->class)) { + if ($node->class instanceof Name) { + $this->checkBlackList($node->class->toString(), CodeChecker::CLASS_CONST_FETCH_NOT_ALLOWED, $node); + } + if ($node->class instanceof Node\Expr\Variable) { + /** + * TODO: find a way to detect something like this: + * $c = "OC_API"; + * $n = $i::ADMIN_AUTH; + */ + } else { + $this->checkBlackListConstant($node->class->toString(), $node->name, $node); + } + } + } + if ($node instanceof Node\Expr\New_) { + if (!is_null($node->class)) { + if ($node->class instanceof Name) { + $this->checkBlackList($node->class->toString(), CodeChecker::CLASS_NEW_NOT_ALLOWED, $node); + } + if ($node->class instanceof Node\Expr\Variable) { + /** + * TODO: find a way to detect something like this: + * $c = "OC_API"; + * $n = new $i; + */ + } + } + } + if ($node instanceof Node\Stmt\UseUse) { + $this->checkBlackList($node->name->toString(), CodeChecker::CLASS_USE_NOT_ALLOWED, $node); + if ($node->alias) { + $this->addUseNameToBlackList($node->name->toString(), $node->alias); + } else { + $this->addUseNameToBlackList($node->name->toString(), $node->name->getLast()); + } + } + } + + /** + * Check whether an alias was introduced for a namespace of a blacklisted class + * + * Example: + * - Blacklist entry: OCP\AppFramework\IApi + * - Name: OCP\AppFramework + * - Alias: OAF + * => new blacklist entry: OAF\IApi + * + * @param string $name + * @param string $alias + */ + private function addUseNameToBlackList($name, $alias) { + $name = strtolower($name); + $alias = strtolower($alias); + + foreach ($this->blackListedClassNames as $blackListedAlias => $blackListedClassName) { + if (strpos($blackListedClassName, $name . '\\') === 0) { + $aliasedClassName = str_replace($name, $alias, $blackListedClassName); + $this->blackListedClassNames[$aliasedClassName] = $blackListedClassName; + } + } + + foreach ($this->blackListedConstants as $blackListedAlias => $blackListedConstant) { + if (strpos($blackListedConstant, $name . '\\') === 0 || strpos($blackListedConstant, $name . '::') === 0) { + $aliasedConstantName = str_replace($name, $alias, $blackListedConstant); + $this->blackListedConstants[$aliasedConstantName] = $blackListedConstant; + } + } + + foreach ($this->blackListedFunctions as $blackListedAlias => $blackListedFunction) { + if (strpos($blackListedFunction, $name . '\\') === 0 || strpos($blackListedFunction, $name . '::') === 0) { + $aliasedFunctionName = str_replace($name, $alias, $blackListedFunction); + $this->blackListedFunctions[$aliasedFunctionName] = $blackListedFunction; + } + } + + foreach ($this->blackListedMethods as $blackListedAlias => $blackListedMethod) { + if (strpos($blackListedMethod, $name . '\\') === 0 || strpos($blackListedMethod, $name . '::') === 0) { + $aliasedMethodName = str_replace($name, $alias, $blackListedMethod); + $this->blackListedMethods[$aliasedMethodName] = $blackListedMethod; + } + } + } + + private function checkBlackList($name, $errorCode, Node $node) { + $lowerName = strtolower($name); + + if (isset($this->blackListedClassNames[$lowerName])) { + $this->errors[]= [ + 'disallowedToken' => $name, + 'errorCode' => $errorCode, + 'line' => $node->getLine(), + 'reason' => $this->buildReason($this->blackListedClassNames[$lowerName], $errorCode) + ]; + } + } + + private function checkBlackListConstant($class, $constantName, Node $node) { + $name = $class . '::' . $constantName; + $lowerName = strtolower($name); + + if (isset($this->blackListedConstants[$lowerName])) { + $this->errors[]= [ + 'disallowedToken' => $name, + 'errorCode' => CodeChecker::CLASS_CONST_FETCH_NOT_ALLOWED, + 'line' => $node->getLine(), + 'reason' => $this->buildReason($this->blackListedConstants[$lowerName], CodeChecker::CLASS_CONST_FETCH_NOT_ALLOWED) + ]; + } + } + + private function checkBlackListFunction($class, $functionName, Node $node) { + $name = $class . '::' . $functionName; + $lowerName = strtolower($name); + + if (isset($this->blackListedFunctions[$lowerName])) { + $this->errors[]= [ + 'disallowedToken' => $name, + 'errorCode' => CodeChecker::STATIC_CALL_NOT_ALLOWED, + 'line' => $node->getLine(), + 'reason' => $this->buildReason($this->blackListedFunctions[$lowerName], CodeChecker::STATIC_CALL_NOT_ALLOWED) + ]; + } + } + + private function checkBlackListMethod($class, $functionName, Node $node) { + $name = $class . '::' . $functionName; + $lowerName = strtolower($name); + + if (isset($this->blackListedMethods[$lowerName])) { + $this->errors[]= [ + 'disallowedToken' => $name, + 'errorCode' => CodeChecker::CLASS_METHOD_CALL_NOT_ALLOWED, + 'line' => $node->getLine(), + 'reason' => $this->buildReason($this->blackListedMethods[$lowerName], CodeChecker::CLASS_METHOD_CALL_NOT_ALLOWED) + ]; + } + } + + private function buildReason($name, $errorCode) { + if (isset($this->errorMessages[$errorCode])) { + $desc = $this->list->getDescription($errorCode, $name); + return sprintf($this->errorMessages[$errorCode], $desc); + } + + return "$name usage not allowed - error: $errorCode"; + } +} diff --git a/lib/private/App/CodeChecker/PrivateCheck.php b/lib/private/App/CodeChecker/PrivateCheck.php new file mode 100644 index 00000000000..32248ab21e2 --- /dev/null +++ b/lib/private/App/CodeChecker/PrivateCheck.php @@ -0,0 +1,84 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\App\CodeChecker; + +class PrivateCheck extends AbstractCheck implements ICheck { + /** + * @return string + */ + protected function getLocalDescription() { + return 'private'; + } + + /** + * @return array + */ + public function getLocalClasses() { + return [ + // classes replaced by the public api + 'OC_API' => '6.0.0', + 'OC_App' => '6.0.0', + 'OC_AppConfig' => '6.0.0', + 'OC_Avatar' => '6.0.0', + 'OC_BackgroundJob' => '6.0.0', + 'OC_Config' => '6.0.0', + 'OC_DB' => '6.0.0', + 'OC_Files' => '6.0.0', + 'OC_Helper' => '6.0.0', + 'OC_Hook' => '6.0.0', + 'OC_Image' => '6.0.0', + 'OC_JSON' => '6.0.0', + 'OC_L10N' => '6.0.0', + 'OC_Log' => '6.0.0', + 'OC_Mail' => '6.0.0', + 'OC_Preferences' => '6.0.0', + 'OC_Search_Provider' => '6.0.0', + 'OC_Search_Result' => '6.0.0', + 'OC_Request' => '6.0.0', + 'OC_Response' => '6.0.0', + 'OC_Template' => '6.0.0', + 'OC_User' => '6.0.0', + 'OC_Util' => '6.0.0', + ]; + } + + /** + * @return array + */ + public function getLocalConstants() { + return []; + } + + /** + * @return array + */ + public function getLocalFunctions() { + return []; + } + + /** + * @return array + */ + public function getLocalMethods() { + return []; + } +} diff --git a/lib/private/App/CodeChecker/StrongComparisonCheck.php b/lib/private/App/CodeChecker/StrongComparisonCheck.php new file mode 100644 index 00000000000..919647a6a54 --- /dev/null +++ b/lib/private/App/CodeChecker/StrongComparisonCheck.php @@ -0,0 +1,78 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\App\CodeChecker; + +class StrongComparisonCheck implements ICheck { + /** @var ICheck */ + protected $check; + + /** + * @param ICheck $check + */ + public function __construct(ICheck $check) { + $this->check = $check; + } + + /** + * @param int $errorCode + * @param string $errorObject + * @return string + */ + public function getDescription($errorCode, $errorObject) { + return $this->check->getDescription($errorCode, $errorObject); + } + + /** + * @return array + */ + public function getClasses() { + return $this->check->getClasses(); + } + + /** + * @return array + */ + public function getConstants() { + return $this->check->getConstants(); + } + + /** + * @return array + */ + public function getFunctions() { + return $this->check->getFunctions(); + } + + /** + * @return array + */ + public function getMethods() { + return $this->check->getMethods(); + } + + /** + * @return bool + */ + public function checkStrongComparisons() { + return true; + } +} diff --git a/lib/private/App/DependencyAnalyzer.php b/lib/private/App/DependencyAnalyzer.php new file mode 100644 index 00000000000..6519e15bd8b --- /dev/null +++ b/lib/private/App/DependencyAnalyzer.php @@ -0,0 +1,319 @@ +<?php +/** + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\App; + +use OCP\IL10N; + +class DependencyAnalyzer { + + /** @var Platform */ + private $platform; + /** @var \OCP\IL10N */ + private $l; + /** @var array */ + private $appInfo; + + /** + * @param Platform $platform + * @param \OCP\IL10N $l + */ + function __construct(Platform $platform, IL10N $l) { + $this->platform = $platform; + $this->l = $l; + } + + /** + * @param array $app + * @returns array of missing dependencies + */ + public function analyze(array $app) { + $this->appInfo = $app; + if (isset($app['dependencies'])) { + $dependencies = $app['dependencies']; + } else { + $dependencies = []; + } + + return array_merge( + $this->analyzePhpVersion($dependencies), + $this->analyzeDatabases($dependencies), + $this->analyzeCommands($dependencies), + $this->analyzeLibraries($dependencies), + $this->analyzeOS($dependencies), + $this->analyzeOC($dependencies, $app) + ); + } + + /** + * Truncates both versions to the lowest common version, e.g. + * 5.1.2.3 and 5.1 will be turned into 5.1 and 5.1, + * 5.2.6.5 and 5.1 will be turned into 5.2 and 5.1 + * @param string $first + * @param string $second + * @return string[] first element is the first version, second element is the + * second version + */ + private function normalizeVersions($first, $second) { + $first = explode('.', $first); + $second = explode('.', $second); + + // get both arrays to the same minimum size + $length = min(count($second), count($first)); + $first = array_slice($first, 0, $length); + $second = array_slice($second, 0, $length); + + return [implode('.', $first), implode('.', $second)]; + } + + /** + * Parameters will be normalized and then passed into version_compare + * in the same order they are specified in the method header + * @param string $first + * @param string $second + * @param string $operator + * @return bool result similar to version_compare + */ + private function compare($first, $second, $operator) { + // we can't normalize versions if one of the given parameters is not a + // version string but null. In case one parameter is null normalization + // will therefore be skipped + if ($first !== null && $second !== null) { + list($first, $second) = $this->normalizeVersions($first, $second); + } + + return version_compare($first, $second, $operator); + } + + /** + * Checks if a version is bigger than another version + * @param string $first + * @param string $second + * @return bool true if the first version is bigger than the second + */ + private function compareBigger($first, $second) { + return $this->compare($first, $second, '>'); + } + + /** + * Checks if a version is smaller than another version + * @param string $first + * @param string $second + * @return bool true if the first version is smaller than the second + */ + private function compareSmaller($first, $second) { + return $this->compare($first, $second, '<'); + } + + /** + * @param array $dependencies + * @return array + */ + private function analyzePhpVersion(array $dependencies) { + $missing = []; + if (isset($dependencies['php']['@attributes']['min-version'])) { + $minVersion = $dependencies['php']['@attributes']['min-version']; + if ($this->compareSmaller($this->platform->getPhpVersion(), $minVersion)) { + $missing[] = (string)$this->l->t('PHP %s or higher is required.', $minVersion); + } + } + if (isset($dependencies['php']['@attributes']['max-version'])) { + $maxVersion = $dependencies['php']['@attributes']['max-version']; + if ($this->compareBigger($this->platform->getPhpVersion(), $maxVersion)) { + $missing[] = (string)$this->l->t('PHP with a version lower than %s is required.', $maxVersion); + } + } + return $missing; + } + + /** + * @param array $dependencies + * @return array + */ + private function analyzeDatabases(array $dependencies) { + $missing = []; + if (!isset($dependencies['database'])) { + return $missing; + } + + $supportedDatabases = $dependencies['database']; + if (empty($supportedDatabases)) { + return $missing; + } + if (!is_array($supportedDatabases)) { + $supportedDatabases = array($supportedDatabases); + } + $supportedDatabases = array_map(function ($db) { + return $this->getValue($db); + }, $supportedDatabases); + $currentDatabase = $this->platform->getDatabase(); + if (!in_array($currentDatabase, $supportedDatabases)) { + $missing[] = (string)$this->l->t('Following databases are supported: %s', join(', ', $supportedDatabases)); + } + return $missing; + } + + /** + * @param array $dependencies + * @return array + */ + private function analyzeCommands(array $dependencies) { + $missing = []; + if (!isset($dependencies['command'])) { + return $missing; + } + + $commands = $dependencies['command']; + if (!is_array($commands)) { + $commands = array($commands); + } + $os = $this->platform->getOS(); + foreach ($commands as $command) { + if (isset($command['@attributes']['os']) && $command['@attributes']['os'] !== $os) { + continue; + } + $commandName = $this->getValue($command); + if (!$this->platform->isCommandKnown($commandName)) { + $missing[] = (string)$this->l->t('The command line tool %s could not be found', $commandName); + } + } + return $missing; + } + + /** + * @param array $dependencies + * @return array + */ + private function analyzeLibraries(array $dependencies) { + $missing = []; + if (!isset($dependencies['lib'])) { + return $missing; + } + + $libs = $dependencies['lib']; + if (!is_array($libs)) { + $libs = array($libs); + } + foreach ($libs as $lib) { + $libName = $this->getValue($lib); + $libVersion = $this->platform->getLibraryVersion($libName); + if (is_null($libVersion)) { + $missing[] = (string)$this->l->t('The library %s is not available.', $libName); + continue; + } + + if (is_array($lib)) { + if (isset($lib['@attributes']['min-version'])) { + $minVersion = $lib['@attributes']['min-version']; + if ($this->compareSmaller($libVersion, $minVersion)) { + $missing[] = (string)$this->l->t('Library %s with a version higher than %s is required - available version %s.', + array($libName, $minVersion, $libVersion)); + } + } + if (isset($lib['@attributes']['max-version'])) { + $maxVersion = $lib['@attributes']['max-version']; + if ($this->compareBigger($libVersion, $maxVersion)) { + $missing[] = (string)$this->l->t('Library %s with a version lower than %s is required - available version %s.', + array($libName, $maxVersion, $libVersion)); + } + } + } + } + return $missing; + } + + /** + * @param array $dependencies + * @return array + */ + private function analyzeOS(array $dependencies) { + $missing = []; + if (!isset($dependencies['os'])) { + return $missing; + } + + $oss = $dependencies['os']; + if (empty($oss)) { + return $missing; + } + if (is_array($oss)) { + $oss = array_map(function ($os) { + return $this->getValue($os); + }, $oss); + } else { + $oss = array($oss); + } + $currentOS = $this->platform->getOS(); + if (!in_array($currentOS, $oss)) { + $missing[] = (string)$this->l->t('Following platforms are supported: %s', join(', ', $oss)); + } + return $missing; + } + + /** + * @param array $dependencies + * @param array $appInfo + * @return array + */ + private function analyzeOC(array $dependencies, array $appInfo) { + $missing = []; + $minVersion = null; + if (isset($dependencies['owncloud']['@attributes']['min-version'])) { + $minVersion = $dependencies['owncloud']['@attributes']['min-version']; + } elseif (isset($appInfo['requiremin'])) { + $minVersion = $appInfo['requiremin']; + } elseif (isset($appInfo['require'])) { + $minVersion = $appInfo['require']; + } + $maxVersion = null; + if (isset($dependencies['owncloud']['@attributes']['max-version'])) { + $maxVersion = $dependencies['owncloud']['@attributes']['max-version']; + } elseif (isset($appInfo['requiremax'])) { + $maxVersion = $appInfo['requiremax']; + } + + if (!is_null($minVersion)) { + if ($this->compareSmaller($this->platform->getOcVersion(), $minVersion)) { + $missing[] = (string)$this->l->t('ownCloud %s or higher is required.', $minVersion); + } + } + if (!is_null($maxVersion)) { + if ($this->compareBigger($this->platform->getOcVersion(), $maxVersion)) { + $missing[] = (string)$this->l->t('ownCloud %s or lower is required.', $maxVersion); + } + } + return $missing; + } + + /** + * @param $element + * @return mixed + */ + private function getValue($element) { + if (isset($element['@value'])) + return $element['@value']; + return (string)$element; + } +} diff --git a/lib/private/App/InfoParser.php b/lib/private/App/InfoParser.php new file mode 100644 index 00000000000..c33e5349f3b --- /dev/null +++ b/lib/private/App/InfoParser.php @@ -0,0 +1,164 @@ +<?php +/** + * @author Andreas Fischer <bantu@owncloud.com> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\App; + +use OCP\IURLGenerator; + +class InfoParser { + /** + * @var \OC\HTTPHelper + */ + private $httpHelper; + + /** + * @var IURLGenerator + */ + private $urlGenerator; + + /** + * @param \OC\HTTPHelper $httpHelper + * @param IURLGenerator $urlGenerator + */ + public function __construct(\OC\HTTPHelper $httpHelper, IURLGenerator $urlGenerator) { + $this->httpHelper = $httpHelper; + $this->urlGenerator = $urlGenerator; + } + + /** + * @param string $file the xml file to be loaded + * @return null|array where null is an indicator for an error + */ + public function parse($file) { + if (!file_exists($file)) { + return null; + } + + libxml_use_internal_errors(true); + $loadEntities = libxml_disable_entity_loader(false); + $xml = simplexml_load_file($file); + libxml_disable_entity_loader($loadEntities); + if ($xml == false) { + libxml_clear_errors(); + return null; + } + $array = $this->xmlToArray($xml); + if (is_null($array)) { + return null; + } + if (!array_key_exists('info', $array)) { + $array['info'] = array(); + } + if (!array_key_exists('remote', $array)) { + $array['remote'] = array(); + } + if (!array_key_exists('public', $array)) { + $array['public'] = array(); + } + if (!array_key_exists('types', $array)) { + $array['types'] = array(); + } + + if (array_key_exists('documentation', $array) && is_array($array['documentation'])) { + foreach ($array['documentation'] as $key => $url) { + // If it is not an absolute URL we assume it is a key + // i.e. admin-ldap will get converted to go.php?to=admin-ldap + if (!$this->httpHelper->isHTTPURL($url)) { + $url = $this->urlGenerator->linkToDocs($url); + } + + $array['documentation'][$key] = $url; + } + } + if (array_key_exists('types', $array)) { + if (is_array($array['types'])) { + foreach ($array['types'] as $type => $v) { + unset($array['types'][$type]); + if (is_string($type)) { + $array['types'][] = $type; + } + } + } else { + $array['types'] = array(); + } + } + + return $array; + } + + /** + * @param \SimpleXMLElement $xml + * @return array + */ + function xmlToArray($xml) { + if (!$xml->children()) { + return (string)$xml; + } + + $array = array(); + foreach ($xml->children() as $element => $node) { + $totalElement = count($xml->{$element}); + + if (!isset($array[$element])) { + $array[$element] = ""; + } + /** + * @var \SimpleXMLElement $node + */ + + // Has attributes + if ($attributes = $node->attributes()) { + $data = array( + '@attributes' => array(), + ); + if (!count($node->children())){ + $value = (string)$node; + if (!empty($value)) { + $data['@value'] = (string)$node; + } + } else { + $data = array_merge($data, $this->xmlToArray($node)); + } + foreach ($attributes as $attr => $value) { + $data['@attributes'][$attr] = (string)$value; + } + + if ($totalElement > 1) { + $array[$element][] = $data; + } else { + $array[$element] = $data; + } + // Just a value + } else { + if ($totalElement > 1) { + $array[$element][] = $this->xmlToArray($node); + } else { + $array[$element] = $this->xmlToArray($node); + } + } + } + + return $array; + } +} diff --git a/lib/private/App/Platform.php b/lib/private/App/Platform.php new file mode 100644 index 00000000000..1d4c3767121 --- /dev/null +++ b/lib/private/App/Platform.php @@ -0,0 +1,92 @@ +<?php +/** + * @author Morris Jobke <hey@morrisjobke.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\App; + +use OC_Util; +use OCP\IConfig; + +/** + * Class Platform + * + * This class basically abstracts any kind of information which can be retrieved from the underlying system. + * + * @package OC\App + */ +class Platform { + + /** + * @param IConfig $config + */ + function __construct(IConfig $config) { + $this->config = $config; + } + + /** + * @return string + */ + public function getPhpVersion() { + return phpversion(); + } + + /** + * @return string + */ + public function getOcVersion() { + $v = \OCP\Util::getVersion(); + return join('.', $v); + } + + /** + * @return string + */ + public function getDatabase() { + $dbType = $this->config->getSystemValue('dbtype', 'sqlite'); + if ($dbType === 'sqlite3') { + $dbType = 'sqlite'; + } + + return $dbType; + } + + /** + * @return string + */ + public function getOS() { + return php_uname('s'); + } + + /** + * @param $command + * @return bool + */ + public function isCommandKnown($command) { + $path = \OC_Helper::findBinaryPath($command); + return ($path !== null); + } + + public function getLibraryVersion($name) { + $repo = new PlatformRepository(); + $lib = $repo->findLibrary($name); + return $lib; + } +} diff --git a/lib/private/App/PlatformRepository.php b/lib/private/App/PlatformRepository.php new file mode 100644 index 00000000000..7363b2a44b1 --- /dev/null +++ b/lib/private/App/PlatformRepository.php @@ -0,0 +1,228 @@ +<?php +/** + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +namespace OC\App; + +/** + * Class PlatformRepository + * + * Inspired by the composer project - licensed under MIT + * https://github.com/composer/composer/blob/master/src/Composer/Repository/PlatformRepository.php#L82 + * + * @package OC\App + */ +class PlatformRepository { + public function __construct() { + $this->packages = $this->initialize(); + } + + protected function initialize() { + $loadedExtensions = get_loaded_extensions(); + $packages = array(); + + // Extensions scanning + foreach ($loadedExtensions as $name) { + if (in_array($name, array('standard', 'Core'))) { + continue; + } + + $ext = new \ReflectionExtension($name); + try { + $prettyVersion = $ext->getVersion(); + $prettyVersion = $this->normalizeVersion($prettyVersion); + } catch (\UnexpectedValueException $e) { + $prettyVersion = '0'; + $prettyVersion = $this->normalizeVersion($prettyVersion); + } + + $packages[$this->buildPackageName($name)] = $prettyVersion; + } + + foreach ($loadedExtensions as $name) { + $prettyVersion = null; + switch ($name) { + case 'curl': + $curlVersion = curl_version(); + $prettyVersion = $curlVersion['version']; + break; + + case 'iconv': + $prettyVersion = ICONV_VERSION; + break; + + case 'intl': + $name = 'ICU'; + if (defined('INTL_ICU_VERSION')) { + $prettyVersion = INTL_ICU_VERSION; + } else { + $reflector = new \ReflectionExtension('intl'); + + ob_start(); + $reflector->info(); + $output = ob_get_clean(); + + preg_match('/^ICU version => (.*)$/m', $output, $matches); + $prettyVersion = $matches[1]; + } + + break; + + case 'libxml': + $prettyVersion = LIBXML_DOTTED_VERSION; + break; + + case 'openssl': + $prettyVersion = preg_replace_callback('{^(?:OpenSSL\s*)?([0-9.]+)([a-z]?).*}', function ($match) { + return $match[1] . (empty($match[2]) ? '' : '.' . (ord($match[2]) - 96)); + }, OPENSSL_VERSION_TEXT); + break; + + case 'pcre': + $prettyVersion = preg_replace('{^(\S+).*}', '$1', PCRE_VERSION); + break; + + case 'uuid': + $prettyVersion = phpversion('uuid'); + break; + + case 'xsl': + $prettyVersion = LIBXSLT_DOTTED_VERSION; + break; + + default: + // None handled extensions have no special cases, skip + continue 2; + } + + try { + $prettyVersion = $this->normalizeVersion($prettyVersion); + } catch (\UnexpectedValueException $e) { + continue; + } + + $packages[$this->buildPackageName($name)] = $prettyVersion; + } + + return $packages; + } + + private function buildPackageName($name) { + return str_replace(' ', '-', $name); + } + + /** + * @param $name + * @return string + */ + public function findLibrary($name) { + $extName = $this->buildPackageName($name); + if (isset($this->packages[$extName])) { + return $this->packages[$extName]; + } + return null; + } + + private static $modifierRegex = '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)(?:[.-]?(\d+))?)?([.-]?dev)?'; + + /** + * Normalizes a version string to be able to perform comparisons on it + * + * https://github.com/composer/composer/blob/master/src/Composer/Package/Version/VersionParser.php#L94 + * + * @param string $version + * @param string $fullVersion optional complete version string to give more context + * @throws \UnexpectedValueException + * @return string + */ + public function normalizeVersion($version, $fullVersion = null) { + $version = trim($version); + if (null === $fullVersion) { + $fullVersion = $version; + } + // ignore aliases and just assume the alias is required instead of the source + if (preg_match('{^([^,\s]+) +as +([^,\s]+)$}', $version, $match)) { + $version = $match[1]; + } + // match master-like branches + if (preg_match('{^(?:dev-)?(?:master|trunk|default)$}i', $version)) { + return '9999999-dev'; + } + if ('dev-' === strtolower(substr($version, 0, 4))) { + return 'dev-' . substr($version, 4); + } + // match classical versioning + if (preg_match('{^v?(\d{1,3})(\.\d+)?(\.\d+)?(\.\d+)?' . self::$modifierRegex . '$}i', $version, $matches)) { + $version = $matches[1] + . (!empty($matches[2]) ? $matches[2] : '.0') + . (!empty($matches[3]) ? $matches[3] : '.0') + . (!empty($matches[4]) ? $matches[4] : '.0'); + $index = 5; + } elseif (preg_match('{^v?(\d{4}(?:[.:-]?\d{2}){1,6}(?:[.:-]?\d{1,3})?)' . self::$modifierRegex . '$}i', $version, $matches)) { // match date-based versioning + $version = preg_replace('{\D}', '-', $matches[1]); + $index = 2; + } elseif (preg_match('{^v?(\d{4,})(\.\d+)?(\.\d+)?(\.\d+)?' . self::$modifierRegex . '$}i', $version, $matches)) { + $version = $matches[1] + . (!empty($matches[2]) ? $matches[2] : '.0') + . (!empty($matches[3]) ? $matches[3] : '.0') + . (!empty($matches[4]) ? $matches[4] : '.0'); + $index = 5; + } + // add version modifiers if a version was matched + if (isset($index)) { + if (!empty($matches[$index])) { + if ('stable' === $matches[$index]) { + return $version; + } + $version .= '-' . $this->expandStability($matches[$index]) . (!empty($matches[$index + 1]) ? $matches[$index + 1] : ''); + } + if (!empty($matches[$index + 2])) { + $version .= '-dev'; + } + return $version; + } + $extraMessage = ''; + if (preg_match('{ +as +' . preg_quote($version) . '$}', $fullVersion)) { + $extraMessage = ' in "' . $fullVersion . '", the alias must be an exact version'; + } elseif (preg_match('{^' . preg_quote($version) . ' +as +}', $fullVersion)) { + $extraMessage = ' in "' . $fullVersion . '", the alias source must be an exact version, if it is a branch name you should prefix it with dev-'; + } + throw new \UnexpectedValueException('Invalid version string "' . $version . '"' . $extraMessage); + } + + /** + * @param string $stability + */ + private function expandStability($stability) { + $stability = strtolower($stability); + switch ($stability) { + case 'a': + return 'alpha'; + case 'b': + return 'beta'; + case 'p': + case 'pl': + return 'patch'; + case 'rc': + return 'RC'; + default: + return $stability; + } + } +} |