diff options
Diffstat (limited to 'lib/private/legacy')
-rw-r--r-- | lib/private/legacy/api.php | 533 | ||||
-rw-r--r-- | lib/private/legacy/app.php | 1284 | ||||
-rw-r--r-- | lib/private/legacy/archive.php | 158 | ||||
-rw-r--r-- | lib/private/legacy/db.php | 258 | ||||
-rw-r--r-- | lib/private/legacy/defaults.php | 286 | ||||
-rw-r--r-- | lib/private/legacy/eventsource.php | 125 | ||||
-rw-r--r-- | lib/private/legacy/filechunking.php | 175 | ||||
-rw-r--r-- | lib/private/legacy/files.php | 316 | ||||
-rw-r--r-- | lib/private/legacy/group.php | 300 | ||||
-rw-r--r-- | lib/private/legacy/helper.php | 703 | ||||
-rw-r--r-- | lib/private/legacy/hook.php | 145 | ||||
-rw-r--r-- | lib/private/legacy/image.php | 1147 | ||||
-rw-r--r-- | lib/private/legacy/installer.php | 638 | ||||
-rw-r--r-- | lib/private/legacy/json.php | 179 | ||||
-rw-r--r-- | lib/private/legacy/ocs.php | 42 | ||||
-rw-r--r-- | lib/private/legacy/response.php | 268 | ||||
-rw-r--r-- | lib/private/legacy/template.php | 433 | ||||
-rw-r--r-- | lib/private/legacy/user.php | 639 | ||||
-rw-r--r-- | lib/private/legacy/util.php | 1450 |
19 files changed, 9079 insertions, 0 deletions
diff --git a/lib/private/legacy/api.php b/lib/private/legacy/api.php new file mode 100644 index 00000000000..bab879c95f8 --- /dev/null +++ b/lib/private/legacy/api.php @@ -0,0 +1,533 @@ +<?php +/** + * @author Bart Visscher <bartv@thisnet.nl> + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Bjรถrn Schieรle <schiessle@owncloud.com> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Jรถrn Friedrich Dreyer <jfd@butonic.de> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Michael Gapczynski <GapczynskiM@gmail.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Thomas Mรผller <thomas.mueller@tmit.eu> + * @author Tom Needham <tom@owncloud.com> + * @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/> + * + */ +use OCP\API; +use OCP\AppFramework\Http; + +/** + * @author Bart Visscher <bartv@thisnet.nl> + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Bjรถrn Schieรle <schiessle@owncloud.com> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Jรถrn Friedrich Dreyer <jfd@butonic.de> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Michael Gapczynski <GapczynskiM@gmail.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Thomas Mรผller <thomas.mueller@tmit.eu> + * @author Tom Needham <tom@owncloud.com> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2015, 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/> + * + */ + +class OC_API { + + /** + * API authentication levels + */ + + /** @deprecated Use \OCP\API::GUEST_AUTH instead */ + const GUEST_AUTH = 0; + + /** @deprecated Use \OCP\API::USER_AUTH instead */ + const USER_AUTH = 1; + + /** @deprecated Use \OCP\API::SUBADMIN_AUTH instead */ + const SUBADMIN_AUTH = 2; + + /** @deprecated Use \OCP\API::ADMIN_AUTH instead */ + const ADMIN_AUTH = 3; + + /** + * API Response Codes + */ + + /** @deprecated Use \OCP\API::RESPOND_UNAUTHORISED instead */ + const RESPOND_UNAUTHORISED = 997; + + /** @deprecated Use \OCP\API::RESPOND_SERVER_ERROR instead */ + const RESPOND_SERVER_ERROR = 996; + + /** @deprecated Use \OCP\API::RESPOND_NOT_FOUND instead */ + const RESPOND_NOT_FOUND = 998; + + /** @deprecated Use \OCP\API::RESPOND_UNKNOWN_ERROR instead */ + const RESPOND_UNKNOWN_ERROR = 999; + + /** + * api actions + */ + protected static $actions = array(); + private static $logoutRequired = false; + private static $isLoggedIn = false; + + /** + * registers an api call + * @param string $method the http method + * @param string $url the url to match + * @param callable $action the function to run + * @param string $app the id of the app registering the call + * @param int $authLevel the level of authentication required for the call + * @param array $defaults + * @param array $requirements + */ + public static function register($method, $url, $action, $app, + $authLevel = API::USER_AUTH, + $defaults = array(), + $requirements = array()) { + $name = strtolower($method).$url; + $name = str_replace(array('/', '{', '}'), '_', $name); + if(!isset(self::$actions[$name])) { + $oldCollection = OC::$server->getRouter()->getCurrentCollection(); + OC::$server->getRouter()->useCollection('ocs'); + OC::$server->getRouter()->create($name, $url) + ->method($method) + ->defaults($defaults) + ->requirements($requirements) + ->action('OC_API', 'call'); + self::$actions[$name] = array(); + OC::$server->getRouter()->useCollection($oldCollection); + } + self::$actions[$name][] = array('app' => $app, 'action' => $action, 'authlevel' => $authLevel); + } + + /** + * handles an api call + * @param array $parameters + */ + public static function call($parameters) { + $request = \OC::$server->getRequest(); + $method = $request->getMethod(); + + // Prepare the request variables + if($method === 'PUT') { + $parameters['_put'] = $request->getParams(); + } else if($method === 'DELETE') { + $parameters['_delete'] = $request->getParams(); + } + $name = $parameters['_route']; + // Foreach registered action + $responses = array(); + foreach(self::$actions[$name] as $action) { + // Check authentication and availability + if(!self::isAuthorised($action)) { + $responses[] = array( + 'app' => $action['app'], + 'response' => new OC_OCS_Result(null, API::RESPOND_UNAUTHORISED, 'Unauthorised'), + 'shipped' => OC_App::isShipped($action['app']), + ); + continue; + } + if(!is_callable($action['action'])) { + $responses[] = array( + 'app' => $action['app'], + 'response' => new OC_OCS_Result(null, API::RESPOND_NOT_FOUND, 'Api method not found'), + 'shipped' => OC_App::isShipped($action['app']), + ); + continue; + } + // Run the action + $responses[] = array( + 'app' => $action['app'], + 'response' => call_user_func($action['action'], $parameters), + 'shipped' => OC_App::isShipped($action['app']), + ); + } + $response = self::mergeResponses($responses); + $format = self::requestedFormat(); + if (self::$logoutRequired) { + \OC::$server->getUserSession()->logout(); + } + + self::respond($response, $format); + } + + /** + * merge the returned result objects into one response + * @param array $responses + * @return OC_OCS_Result + */ + public static function mergeResponses($responses) { + // Sort into shipped and third-party + $shipped = array( + 'succeeded' => array(), + 'failed' => array(), + ); + $thirdparty = array( + 'succeeded' => array(), + 'failed' => array(), + ); + + foreach($responses as $response) { + if($response['shipped'] || ($response['app'] === 'core')) { + if($response['response']->succeeded()) { + $shipped['succeeded'][$response['app']] = $response; + } else { + $shipped['failed'][$response['app']] = $response; + } + } else { + if($response['response']->succeeded()) { + $thirdparty['succeeded'][$response['app']] = $response; + } else { + $thirdparty['failed'][$response['app']] = $response; + } + } + } + + // Remove any error responses if there is one shipped response that succeeded + if(!empty($shipped['failed'])) { + // Which shipped response do we use if they all failed? + // They may have failed for different reasons (different status codes) + // Which response code should we return? + // Maybe any that are not \OCP\API::RESPOND_SERVER_ERROR + // Merge failed responses if more than one + $data = array(); + foreach($shipped['failed'] as $failure) { + $data = array_merge_recursive($data, $failure['response']->getData()); + } + $picked = reset($shipped['failed']); + $code = $picked['response']->getStatusCode(); + $meta = $picked['response']->getMeta(); + $headers = $picked['response']->getHeaders(); + $response = new OC_OCS_Result($data, $code, $meta['message'], $headers); + return $response; + } elseif(!empty($shipped['succeeded'])) { + $responses = array_merge($shipped['succeeded'], $thirdparty['succeeded']); + } elseif(!empty($thirdparty['failed'])) { + // Merge failed responses if more than one + $data = array(); + foreach($thirdparty['failed'] as $failure) { + $data = array_merge_recursive($data, $failure['response']->getData()); + } + $picked = reset($thirdparty['failed']); + $code = $picked['response']->getStatusCode(); + $meta = $picked['response']->getMeta(); + $headers = $picked['response']->getHeaders(); + $response = new OC_OCS_Result($data, $code, $meta['message'], $headers); + return $response; + } else { + $responses = $thirdparty['succeeded']; + } + // Merge the successful responses + $data = []; + $codes = []; + $header = []; + + foreach($responses as $response) { + if($response['shipped']) { + $data = array_merge_recursive($response['response']->getData(), $data); + } else { + $data = array_merge_recursive($data, $response['response']->getData()); + } + $header = array_merge_recursive($header, $response['response']->getHeaders()); + $codes[] = ['code' => $response['response']->getStatusCode(), + 'meta' => $response['response']->getMeta()]; + } + + // Use any non 100 status codes + $statusCode = 100; + $statusMessage = null; + foreach($codes as $code) { + if($code['code'] != 100) { + $statusCode = $code['code']; + $statusMessage = $code['meta']['message']; + break; + } + } + + return new OC_OCS_Result($data, $statusCode, $statusMessage, $header); + } + + /** + * authenticate the api call + * @param array $action the action details as supplied to OC_API::register() + * @return bool + */ + private static function isAuthorised($action) { + $level = $action['authlevel']; + switch($level) { + case API::GUEST_AUTH: + // Anyone can access + return true; + case API::USER_AUTH: + // User required + return self::loginUser(); + case API::SUBADMIN_AUTH: + // Check for subadmin + $user = self::loginUser(); + if(!$user) { + return false; + } else { + $userObject = \OC::$server->getUserSession()->getUser(); + if($userObject === null) { + return false; + } + $isSubAdmin = \OC::$server->getGroupManager()->getSubAdmin()->isSubAdmin($userObject); + $admin = OC_User::isAdminUser($user); + if($isSubAdmin || $admin) { + return true; + } else { + return false; + } + } + case API::ADMIN_AUTH: + // Check for admin + $user = self::loginUser(); + if(!$user) { + return false; + } else { + return OC_User::isAdminUser($user); + } + default: + // oops looks like invalid level supplied + return false; + } + } + + /** + * http basic auth + * @return string|false (username, or false on failure) + */ + private static function loginUser() { + if(self::$isLoggedIn === true) { + return \OC_User::getUser(); + } + + // reuse existing login + $loggedIn = OC_User::isLoggedIn(); + if ($loggedIn === true) { + $ocsApiRequest = isset($_SERVER['HTTP_OCS_APIREQUEST']) ? $_SERVER['HTTP_OCS_APIREQUEST'] === 'true' : false; + if ($ocsApiRequest) { + + // initialize the user's filesystem + \OC_Util::setUpFS(\OC_User::getUser()); + self::$isLoggedIn = true; + + return OC_User::getUser(); + } + return false; + } + + // basic auth - because OC_User::login will create a new session we shall only try to login + // if user and pass are set + if(isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW']) ) { + $authUser = $_SERVER['PHP_AUTH_USER']; + $authPw = $_SERVER['PHP_AUTH_PW']; + $return = OC_User::login($authUser, $authPw); + if ($return === true) { + self::$logoutRequired = true; + + // initialize the user's filesystem + \OC_Util::setUpFS(\OC_User::getUser()); + self::$isLoggedIn = true; + + /** + * Add DAV authenticated. This should in an ideal world not be + * necessary but the iOS App reads cookies from anywhere instead + * only the DAV endpoint. + * This makes sure that the cookies will be valid for the whole scope + * @see https://github.com/owncloud/core/issues/22893 + */ + \OC::$server->getSession()->set( + \OCA\DAV\Connector\Sabre\Auth::DAV_AUTHENTICATED, + \OC::$server->getUserSession()->getUser()->getUID() + ); + + return \OC_User::getUser(); + } + } + + return false; + } + + /** + * respond to a call + * @param OC_OCS_Result $result + * @param string $format the format xml|json + */ + public static function respond($result, $format='xml') { + $request = \OC::$server->getRequest(); + + // Send 401 headers if unauthorised + if($result->getStatusCode() === API::RESPOND_UNAUTHORISED) { + // If request comes from JS return dummy auth request + if($request->getHeader('X-Requested-With') === 'XMLHttpRequest') { + header('WWW-Authenticate: DummyBasic realm="Authorisation Required"'); + } else { + header('WWW-Authenticate: Basic realm="Authorisation Required"'); + } + header('HTTP/1.0 401 Unauthorized'); + } + + foreach($result->getHeaders() as $name => $value) { + header($name . ': ' . $value); + } + + $meta = $result->getMeta(); + $data = $result->getData(); + if (self::isV2($request)) { + $statusCode = self::mapStatusCodes($result->getStatusCode()); + if (!is_null($statusCode)) { + $meta['statuscode'] = $statusCode; + OC_Response::setStatus($statusCode); + } + } + + self::setContentType($format); + $body = self::renderResult($format, $meta, $data); + echo $body; + } + + /** + * @param XMLWriter $writer + */ + private static function toXML($array, $writer) { + foreach($array as $k => $v) { + if ($k[0] === '@') { + $writer->writeAttribute(substr($k, 1), $v); + continue; + } else if (is_numeric($k)) { + $k = 'element'; + } + if(is_array($v)) { + $writer->startElement($k); + self::toXML($v, $writer); + $writer->endElement(); + } else { + $writer->writeElement($k, $v); + } + } + } + + /** + * @return string + */ + public static function requestedFormat() { + $formats = array('json', 'xml'); + + $format = !empty($_GET['format']) && in_array($_GET['format'], $formats) ? $_GET['format'] : 'xml'; + return $format; + } + + /** + * Based on the requested format the response content type is set + * @param string $format + */ + public static function setContentType($format = null) { + $format = is_null($format) ? self::requestedFormat() : $format; + if ($format === 'xml') { + header('Content-type: text/xml; charset=UTF-8'); + return; + } + + if ($format === 'json') { + header('Content-Type: application/json; charset=utf-8'); + return; + } + + header('Content-Type: application/octet-stream; charset=utf-8'); + } + + /** + * @param \OCP\IRequest $request + * @return bool + */ + protected static function isV2(\OCP\IRequest $request) { + $script = $request->getScriptName(); + + return substr($script, -11) === '/ocs/v2.php'; + } + + /** + * @param integer $sc + * @return int + */ + public static function mapStatusCodes($sc) { + switch ($sc) { + case API::RESPOND_NOT_FOUND: + return Http::STATUS_NOT_FOUND; + case API::RESPOND_SERVER_ERROR: + return Http::STATUS_INTERNAL_SERVER_ERROR; + case API::RESPOND_UNKNOWN_ERROR: + return Http::STATUS_INTERNAL_SERVER_ERROR; + case API::RESPOND_UNAUTHORISED: + // already handled for v1 + return null; + case 100: + return Http::STATUS_OK; + } + // any 2xx, 4xx and 5xx will be used as is + if ($sc >= 200 && $sc < 600) { + return $sc; + } + + return Http::STATUS_BAD_REQUEST; + } + + /** + * @param string $format + * @return string + */ + public static function renderResult($format, $meta, $data) { + $response = array( + 'ocs' => array( + 'meta' => $meta, + 'data' => $data, + ), + ); + if ($format == 'json') { + return OC_JSON::encode($response); + } + + $writer = new XMLWriter(); + $writer->openMemory(); + $writer->setIndent(true); + $writer->startDocument(); + self::toXML($response, $writer); + $writer->endDocument(); + return $writer->outputMemory(true); + } +} diff --git a/lib/private/legacy/app.php b/lib/private/legacy/app.php new file mode 100644 index 00000000000..246bf97ee91 --- /dev/null +++ b/lib/private/legacy/app.php @@ -0,0 +1,1284 @@ +<?php +/** + * @author Arthur Schiwon <blizzz@owncloud.com> + * @author Bart Visscher <bartv@thisnet.nl> + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Bjรถrn Schieรle <schiessle@owncloud.com> + * @author Borjan Tchakaloff <borjan@tchakaloff.fr> + * @author Brice Maron <brice@bmaron.net> + * @author Christopher Schรคpers <kondou@ts.unde.re> + * @author Felix Moeller <mail@felixmoeller.de> + * @author Frank Karlitschek <frank@owncloud.org> + * @author Georg Ehrke <georg@owncloud.com> + * @author Jakob Sack <mail@jakobsack.de> + * @author Jan-Christoph Borchardt <hey@jancborchardt.net> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Jรถrn Friedrich Dreyer <jfd@butonic.de> + * @author Kamil Domanski <kdomanski@kdemail.net> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Markus Goetz <markus@woboq.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author RealRancor <Fisch.666@gmx.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author Roeland Jago Douma <rullzer@owncloud.com> + * @author Sam Tuke <mail@samtuke.com> + * @author Thomas Mรผller <thomas.mueller@tmit.eu> + * @author Thomas Tanghus <thomas@tanghus.net> + * @author Tom Needham <tom@owncloud.com> + * @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/> + * + */ +use OC\App\DependencyAnalyzer; +use OC\App\Platform; +use OC\OCSClient; +use OC\Repair; + +/** + * This class manages the apps. It allows them to register and integrate in the + * ownCloud ecosystem. Furthermore, this class is responsible for installing, + * upgrading and removing apps. + */ +class OC_App { + static private $appVersion = []; + static private $adminForms = array(); + static private $personalForms = array(); + static private $appInfo = array(); + static private $appTypes = array(); + static private $loadedApps = array(); + static private $altLogin = array(); + const officialApp = 200; + + /** + * clean the appId + * + * @param string|boolean $app AppId that needs to be cleaned + * @return string + */ + public static function cleanAppId($app) { + return str_replace(array('\0', '/', '\\', '..'), '', $app); + } + + /** + * Check if an app is loaded + * + * @param string $app + * @return bool + */ + public static function isAppLoaded($app) { + return in_array($app, self::$loadedApps, true); + } + + /** + * loads all apps + * + * @param string[] | string | null $types + * @return bool + * + * This function walks through the ownCloud directory and loads all apps + * it can find. A directory contains an app if the file /appinfo/info.xml + * exists. + * + * if $types is set, only apps of those types will be loaded + */ + public static function loadApps($types = null) { + if (\OC::$server->getSystemConfig()->getValue('maintenance', false)) { + return false; + } + // Load the enabled apps here + $apps = self::getEnabledApps(); + + // Add each apps' folder as allowed class path + foreach($apps as $app) { + $path = self::getAppPath($app); + if($path !== false) { + \OC::$loader->addValidRoot($path); + } + } + + // prevent app.php from printing output + ob_start(); + foreach ($apps as $app) { + if ((is_null($types) or self::isType($app, $types)) && !in_array($app, self::$loadedApps)) { + self::loadApp($app); + } + } + ob_end_clean(); + + return true; + } + + /** + * load a single app + * + * @param string $app + * @param bool $checkUpgrade whether an upgrade check should be done + * @throws \OC\NeedsUpdateException + */ + public static function loadApp($app, $checkUpgrade = true) { + self::$loadedApps[] = $app; + $appPath = self::getAppPath($app); + if($appPath === false) { + return; + } + \OC::$loader->addValidRoot($appPath); // in case someone calls loadApp() directly + if (is_file($appPath . '/appinfo/app.php')) { + \OC::$server->getEventLogger()->start('load_app_' . $app, 'Load app: ' . $app); + if ($checkUpgrade and self::shouldUpgrade($app)) { + throw new \OC\NeedsUpdateException(); + } + self::requireAppFile($app); + if (self::isType($app, array('authentication'))) { + // since authentication apps affect the "is app enabled for group" check, + // the enabled apps cache needs to be cleared to make sure that the + // next time getEnableApps() is called it will also include apps that were + // enabled for groups + self::$enabledAppsCache = array(); + } + \OC::$server->getEventLogger()->end('load_app_' . $app); + } + } + + /** + * Load app.php from the given app + * + * @param string $app app name + */ + private static function requireAppFile($app) { + try { + // encapsulated here to avoid variable scope conflicts + require_once $app . '/appinfo/app.php'; + } catch (Error $ex) { + \OC::$server->getLogger()->logException($ex); + $blacklist = \OC::$server->getAppManager()->getAlwaysEnabledApps(); + if (!in_array($app, $blacklist)) { + self::disable($app); + } + } + } + + /** + * check if an app is of a specific type + * + * @param string $app + * @param string|array $types + * @return bool + */ + public static function isType($app, $types) { + if (is_string($types)) { + $types = array($types); + } + $appTypes = self::getAppTypes($app); + foreach ($types as $type) { + if (array_search($type, $appTypes) !== false) { + return true; + } + } + return false; + } + + /** + * get the types of an app + * + * @param string $app + * @return array + */ + private static function getAppTypes($app) { + //load the cache + if (count(self::$appTypes) == 0) { + self::$appTypes = \OC::$server->getAppConfig()->getValues(false, 'types'); + } + + if (isset(self::$appTypes[$app])) { + return explode(',', self::$appTypes[$app]); + } else { + return array(); + } + } + + /** + * read app types from info.xml and cache them in the database + */ + public static function setAppTypes($app) { + $appData = self::getAppInfo($app); + if(!is_array($appData)) { + return; + } + + if (isset($appData['types'])) { + $appTypes = implode(',', $appData['types']); + } else { + $appTypes = ''; + } + + \OC::$server->getAppConfig()->setValue($app, 'types', $appTypes); + } + + /** + * check if app is shipped + * + * @param string $appId the id of the app to check + * @return bool + * + * Check if an app that is installed is a shipped app or installed from the appstore. + */ + public static function isShipped($appId) { + return \OC::$server->getAppManager()->isShipped($appId); + } + + /** + * get all enabled apps + */ + protected static $enabledAppsCache = array(); + + /** + * Returns apps enabled for the current user. + * + * @param bool $forceRefresh whether to refresh the cache + * @param bool $all whether to return apps for all users, not only the + * currently logged in one + * @return string[] + */ + public static function getEnabledApps($forceRefresh = false, $all = false) { + if (!\OC::$server->getSystemConfig()->getValue('installed', false)) { + return array(); + } + // in incognito mode or when logged out, $user will be false, + // which is also the case during an upgrade + $appManager = \OC::$server->getAppManager(); + if ($all) { + $user = null; + } else { + $user = \OC::$server->getUserSession()->getUser(); + } + + if (is_null($user)) { + $apps = $appManager->getInstalledApps(); + } else { + $apps = $appManager->getEnabledAppsForUser($user); + } + $apps = array_filter($apps, function ($app) { + return $app !== 'files';//we add this manually + }); + sort($apps); + array_unshift($apps, 'files'); + return $apps; + } + + /** + * checks whether or not an app is enabled + * + * @param string $app app + * @return bool + * + * This function checks whether or not an app is enabled. + */ + public static function isEnabled($app) { + return \OC::$server->getAppManager()->isEnabledForUser($app); + } + + /** + * enables an app + * + * @param mixed $app app + * @param array $groups (optional) when set, only these groups will have access to the app + * @throws \Exception + * @return void + * + * This function set an app as enabled in appconfig. + */ + public static function enable($app, $groups = null) { + self::$enabledAppsCache = array(); // flush + if (!OC_Installer::isInstalled($app)) { + $app = self::installApp($app); + } + + $appManager = \OC::$server->getAppManager(); + if (!is_null($groups)) { + $groupManager = \OC::$server->getGroupManager(); + $groupsList = []; + foreach ($groups as $group) { + $groupItem = $groupManager->get($group); + if ($groupItem instanceof \OCP\IGroup) { + $groupsList[] = $groupManager->get($group); + } + } + $appManager->enableAppForGroups($app, $groupsList); + } else { + $appManager->enableApp($app); + } + } + + /** + * @param string $app + * @return int + */ + private static function downloadApp($app) { + $ocsClient = new OCSClient( + \OC::$server->getHTTPClientService(), + \OC::$server->getConfig(), + \OC::$server->getLogger() + ); + $appData = $ocsClient->getApplication($app, \OCP\Util::getVersion()); + $download = $ocsClient->getApplicationDownload($app, \OCP\Util::getVersion()); + if(isset($download['downloadlink']) and $download['downloadlink']!='') { + // Replace spaces in download link without encoding entire URL + $download['downloadlink'] = str_replace(' ', '%20', $download['downloadlink']); + $info = array('source' => 'http', 'href' => $download['downloadlink'], 'appdata' => $appData); + $app = OC_Installer::installApp($info); + } + return $app; + } + + /** + * @param string $app + * @return bool + */ + public static function removeApp($app) { + if (self::isShipped($app)) { + return false; + } + + return OC_Installer::removeApp($app); + } + + /** + * This function set an app as disabled in appconfig. + * + * @param string $app app + * @throws Exception + */ + public static function disable($app) { + // Convert OCS ID to regular application identifier + if(self::getInternalAppIdByOcs($app) !== false) { + $app = self::getInternalAppIdByOcs($app); + } + + self::$enabledAppsCache = array(); // flush + // check if app is a shipped app or not. if not delete + \OC_Hook::emit('OC_App', 'pre_disable', array('app' => $app)); + $appManager = \OC::$server->getAppManager(); + $appManager->disableApp($app); + } + + /** + * Returns the Settings Navigation + * + * @return string[] + * + * This function returns an array containing all settings pages added. The + * entries are sorted by the key 'order' ascending. + */ + public static function getSettingsNavigation() { + $l = \OC::$server->getL10N('lib'); + $urlGenerator = \OC::$server->getURLGenerator(); + + $settings = array(); + // by default, settings only contain the help menu + if (OC_Util::getEditionString() === '' && + \OC::$server->getSystemConfig()->getValue('knowledgebaseenabled', true) == true + ) { + $settings = array( + array( + "id" => "help", + "order" => 1000, + "href" => $urlGenerator->linkToRoute('settings_help'), + "name" => $l->t("Help"), + "icon" => $urlGenerator->imagePath("settings", "help.svg") + ) + ); + } + + // if the user is logged-in + if (OC_User::isLoggedIn()) { + // personal menu + $settings[] = array( + "id" => "personal", + "order" => 1, + "href" => $urlGenerator->linkToRoute('settings_personal'), + "name" => $l->t("Personal"), + "icon" => $urlGenerator->imagePath("settings", "personal.svg") + ); + + //SubAdmins are also allowed to access user management + $userObject = \OC::$server->getUserSession()->getUser(); + $isSubAdmin = false; + if($userObject !== null) { + $isSubAdmin = \OC::$server->getGroupManager()->getSubAdmin()->isSubAdmin($userObject); + } + if ($isSubAdmin) { + // admin users menu + $settings[] = array( + "id" => "core_users", + "order" => 2, + "href" => $urlGenerator->linkToRoute('settings_users'), + "name" => $l->t("Users"), + "icon" => $urlGenerator->imagePath("settings", "users.svg") + ); + } + + // if the user is an admin + if (OC_User::isAdminUser(OC_User::getUser())) { + // admin settings + $settings[] = array( + "id" => "admin", + "order" => 1000, + "href" => $urlGenerator->linkToRoute('settings_admin'), + "name" => $l->t("Admin"), + "icon" => $urlGenerator->imagePath("settings", "admin.svg") + ); + } + } + + $navigation = self::proceedNavigation($settings); + return $navigation; + } + + // This is private as well. It simply works, so don't ask for more details + private static function proceedNavigation($list) { + $activeApp = OC::$server->getNavigationManager()->getActiveEntry(); + foreach ($list as &$navEntry) { + if ($navEntry['id'] == $activeApp) { + $navEntry['active'] = true; + } else { + $navEntry['active'] = false; + } + } + unset($navEntry); + + usort($list, create_function('$a, $b', 'if( $a["order"] == $b["order"] ) {return 0;}elseif( $a["order"] < $b["order"] ) {return -1;}else{return 1;}')); + + return $list; + } + + /** + * Get the path where to install apps + * + * @return string|false + */ + public static function getInstallPath() { + if (\OC::$server->getSystemConfig()->getValue('appstoreenabled', true) == false) { + return false; + } + + foreach (OC::$APPSROOTS as $dir) { + if (isset($dir['writable']) && $dir['writable'] === true) { + return $dir['path']; + } + } + + \OCP\Util::writeLog('core', 'No application directories are marked as writable.', \OCP\Util::ERROR); + return null; + } + + + /** + * search for an app in all app-directories + * + * @param string $appId + * @return false|string + */ + protected static function findAppInDirectories($appId) { + $sanitizedAppId = self::cleanAppId($appId); + if($sanitizedAppId !== $appId) { + return false; + } + static $app_dir = array(); + + if (isset($app_dir[$appId])) { + return $app_dir[$appId]; + } + + $possibleApps = array(); + foreach (OC::$APPSROOTS as $dir) { + if (file_exists($dir['path'] . '/' . $appId)) { + $possibleApps[] = $dir; + } + } + + if (empty($possibleApps)) { + return false; + } elseif (count($possibleApps) === 1) { + $dir = array_shift($possibleApps); + $app_dir[$appId] = $dir; + return $dir; + } else { + $versionToLoad = array(); + foreach ($possibleApps as $possibleApp) { + $version = self::getAppVersionByPath($possibleApp['path']); + if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) { + $versionToLoad = array( + 'dir' => $possibleApp, + 'version' => $version, + ); + } + } + $app_dir[$appId] = $versionToLoad['dir']; + return $versionToLoad['dir']; + //TODO - write test + } + } + + /** + * Get the directory for the given app. + * If the app is defined in multiple directories, the first one is taken. (false if not found) + * + * @param string $appId + * @return string|false + */ + public static function getAppPath($appId) { + if ($appId === null || trim($appId) === '') { + return false; + } + + if (($dir = self::findAppInDirectories($appId)) != false) { + return $dir['path'] . '/' . $appId; + } + return false; + } + + + /** + * check if an app's directory is writable + * + * @param string $appId + * @return bool + */ + public static function isAppDirWritable($appId) { + $path = self::getAppPath($appId); + return ($path !== false) ? is_writable($path) : false; + } + + /** + * Get the path for the given app on the access + * If the app is defined in multiple directories, the first one is taken. (false if not found) + * + * @param string $appId + * @return string|false + */ + public static function getAppWebPath($appId) { + if (($dir = self::findAppInDirectories($appId)) != false) { + return OC::$WEBROOT . $dir['url'] . '/' . $appId; + } + return false; + } + + /** + * get the last version of the app from appinfo/info.xml + * + * @param string $appId + * @return string + */ + public static function getAppVersion($appId) { + if (!isset(self::$appVersion[$appId])) { + $file = self::getAppPath($appId); + self::$appVersion[$appId] = ($file !== false) ? self::getAppVersionByPath($file) : '0'; + } + return self::$appVersion[$appId]; + } + + /** + * get app's version based on it's path + * + * @param string $path + * @return string + */ + public static function getAppVersionByPath($path) { + $infoFile = $path . '/appinfo/info.xml'; + $appData = self::getAppInfo($infoFile, true); + return isset($appData['version']) ? $appData['version'] : ''; + } + + + /** + * Read all app metadata from the info.xml file + * + * @param string $appId id of the app or the path of the info.xml file + * @param boolean $path (optional) + * @return array|null + * @note all data is read from info.xml, not just pre-defined fields + */ + public static function getAppInfo($appId, $path = false) { + if ($path) { + $file = $appId; + } else { + if (isset(self::$appInfo[$appId])) { + return self::$appInfo[$appId]; + } + $appPath = self::getAppPath($appId); + if($appPath === false) { + return null; + } + $file = $appPath . '/appinfo/info.xml'; + } + + $parser = new \OC\App\InfoParser(\OC::$server->getURLGenerator()); + $data = $parser->parse($file); + + if (is_array($data)) { + $data = OC_App::parseAppInfo($data); + } + if(isset($data['ocsid'])) { + $storedId = \OC::$server->getConfig()->getAppValue($appId, 'ocsid'); + if($storedId !== '' && $storedId !== $data['ocsid']) { + $data['ocsid'] = $storedId; + } + } + + self::$appInfo[$appId] = $data; + + return $data; + } + + /** + * Returns the navigation + * + * @return array + * + * This function returns an array containing all entries added. The + * entries are sorted by the key 'order' ascending. Additional to the keys + * given for each app the following keys exist: + * - active: boolean, signals if the user is on this navigation entry + */ + public static function getNavigation() { + $entries = OC::$server->getNavigationManager()->getAll(); + $navigation = self::proceedNavigation($entries); + return $navigation; + } + + /** + * get the id of loaded app + * + * @return string + */ + public static function getCurrentApp() { + $request = \OC::$server->getRequest(); + $script = substr($request->getScriptName(), strlen(OC::$WEBROOT) + 1); + $topFolder = substr($script, 0, strpos($script, '/')); + if (empty($topFolder)) { + $path_info = $request->getPathInfo(); + if ($path_info) { + $topFolder = substr($path_info, 1, strpos($path_info, '/', 1) - 1); + } + } + if ($topFolder == 'apps') { + $length = strlen($topFolder); + return substr($script, $length + 1, strpos($script, '/', $length + 1) - $length - 1); + } else { + return $topFolder; + } + } + + /** + * @param string $type + * @return array + */ + public static function getForms($type) { + $forms = array(); + switch ($type) { + case 'admin': + $source = self::$adminForms; + break; + case 'personal': + $source = self::$personalForms; + break; + default: + return array(); + } + foreach ($source as $form) { + $forms[] = include $form; + } + return $forms; + } + + /** + * register an admin form to be shown + * + * @param string $app + * @param string $page + */ + public static function registerAdmin($app, $page) { + self::$adminForms[] = $app . '/' . $page . '.php'; + } + + /** + * register a personal form to be shown + * @param string $app + * @param string $page + */ + public static function registerPersonal($app, $page) { + self::$personalForms[] = $app . '/' . $page . '.php'; + } + + /** + * @param array $entry + */ + public static function registerLogIn(array $entry) { + self::$altLogin[] = $entry; + } + + /** + * @return array + */ + public static function getAlternativeLogIns() { + return self::$altLogin; + } + + /** + * get a list of all apps in the apps folder + * + * @return array an array of app names (string IDs) + * @todo: change the name of this method to getInstalledApps, which is more accurate + */ + public static function getAllApps() { + + $apps = array(); + + foreach (OC::$APPSROOTS as $apps_dir) { + if (!is_readable($apps_dir['path'])) { + \OCP\Util::writeLog('core', 'unable to read app folder : ' . $apps_dir['path'], \OCP\Util::WARN); + continue; + } + $dh = opendir($apps_dir['path']); + + if (is_resource($dh)) { + while (($file = readdir($dh)) !== false) { + + if ($file[0] != '.' and is_dir($apps_dir['path'] . '/' . $file) and is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')) { + + $apps[] = $file; + } + } + } + } + + return $apps; + } + + /** + * List all apps, this is used in apps.php + * + * @param bool $onlyLocal + * @param bool $includeUpdateInfo Should we check whether there is an update + * in the app store? + * @param OCSClient $ocsClient + * @return array + */ + public static function listAllApps($onlyLocal = false, + $includeUpdateInfo = true, + OCSClient $ocsClient) { + $installedApps = OC_App::getAllApps(); + + //TODO which apps do we want to blacklist and how do we integrate + // blacklisting with the multi apps folder feature? + + //we don't want to show configuration for these + $blacklist = \OC::$server->getAppManager()->getAlwaysEnabledApps(); + $appList = array(); + + foreach ($installedApps as $app) { + if (array_search($app, $blacklist) === false) { + + $info = OC_App::getAppInfo($app); + if (!is_array($info)) { + \OCP\Util::writeLog('core', 'Could not read app info file for app "' . $app . '"', \OCP\Util::ERROR); + continue; + } + + if (!isset($info['name'])) { + \OCP\Util::writeLog('core', 'App id "' . $app . '" has no name in appinfo', \OCP\Util::ERROR); + continue; + } + + $enabled = \OC::$server->getAppConfig()->getValue($app, 'enabled', 'no'); + $info['groups'] = null; + if ($enabled === 'yes') { + $active = true; + } else if ($enabled === 'no') { + $active = false; + } else { + $active = true; + $info['groups'] = $enabled; + } + + $info['active'] = $active; + + if (self::isShipped($app)) { + $info['internal'] = true; + $info['level'] = self::officialApp; + $info['removable'] = false; + } else { + $info['internal'] = false; + $info['removable'] = true; + } + + $info['update'] = ($includeUpdateInfo) ? OC_Installer::isUpdateAvailable($app) : null; + + $appPath = self::getAppPath($app); + if($appPath !== false) { + $appIcon = $appPath . '/img/' . $app . '.svg'; + if (file_exists($appIcon)) { + $info['preview'] = \OC::$server->getURLGenerator()->imagePath($app, $app . '.svg'); + $info['previewAsIcon'] = true; + } else { + $appIcon = $appPath . '/img/app.svg'; + if (file_exists($appIcon)) { + $info['preview'] = \OC::$server->getURLGenerator()->imagePath($app, 'app.svg'); + $info['previewAsIcon'] = true; + } + } + } + $info['version'] = OC_App::getAppVersion($app); + $appList[] = $info; + } + } + if ($onlyLocal) { + $remoteApps = []; + } else { + $remoteApps = OC_App::getAppstoreApps('approved', null, $ocsClient); + } + if ($remoteApps) { + // Remove duplicates + foreach ($appList as $app) { + foreach ($remoteApps AS $key => $remote) { + if ($app['name'] === $remote['name'] || + (isset($app['ocsid']) && + $app['ocsid'] === $remote['id']) + ) { + unset($remoteApps[$key]); + } + } + } + $combinedApps = array_merge($appList, $remoteApps); + } else { + $combinedApps = $appList; + } + + return $combinedApps; + } + + /** + * Returns the internal app ID or false + * @param string $ocsID + * @return string|false + */ + public static function getInternalAppIdByOcs($ocsID) { + if(is_numeric($ocsID)) { + $idArray = \OC::$server->getAppConfig()->getValues(false, 'ocsid'); + if(array_search($ocsID, $idArray)) { + return array_search($ocsID, $idArray); + } + } + return false; + } + + /** + * Get a list of all apps on the appstore + * @param string $filter + * @param string|null $category + * @param OCSClient $ocsClient + * @return array|bool multi-dimensional array of apps. + * Keys: id, name, type, typename, personid, license, detailpage, preview, changed, description + */ + public static function getAppstoreApps($filter = 'approved', + $category = null, + OCSClient $ocsClient) { + $categories = [$category]; + + if (is_null($category)) { + $categoryNames = $ocsClient->getCategories(\OCP\Util::getVersion()); + if (is_array($categoryNames)) { + // Check that categories of apps were retrieved correctly + if (!$categories = array_keys($categoryNames)) { + return false; + } + } else { + return false; + } + } + + $page = 0; + $remoteApps = $ocsClient->getApplications($categories, $page, $filter, \OCP\Util::getVersion()); + $apps = []; + $i = 0; + $l = \OC::$server->getL10N('core'); + foreach ($remoteApps as $app) { + $potentialCleanId = self::getInternalAppIdByOcs($app['id']); + // enhance app info (for example the description) + $apps[$i] = OC_App::parseAppInfo($app); + $apps[$i]['author'] = $app['personid']; + $apps[$i]['ocs_id'] = $app['id']; + $apps[$i]['internal'] = 0; + $apps[$i]['active'] = ($potentialCleanId !== false) ? self::isEnabled($potentialCleanId) : false; + $apps[$i]['update'] = false; + $apps[$i]['groups'] = false; + $apps[$i]['score'] = $app['score']; + $apps[$i]['removable'] = false; + if ($app['label'] == 'recommended') { + $apps[$i]['internallabel'] = (string)$l->t('Recommended'); + $apps[$i]['internalclass'] = 'recommendedapp'; + } + + // Apps from the appstore are always assumed to be compatible with the + // the current release as the initial filtering is done on the appstore + $apps[$i]['dependencies']['owncloud']['@attributes']['min-version'] = implode('.', \OCP\Util::getVersion()); + $apps[$i]['dependencies']['owncloud']['@attributes']['max-version'] = implode('.', \OCP\Util::getVersion()); + + $i++; + } + + + + if (empty($apps)) { + return false; + } else { + return $apps; + } + } + + public static function shouldUpgrade($app) { + $versions = self::getAppVersions(); + $currentVersion = OC_App::getAppVersion($app); + if ($currentVersion && isset($versions[$app])) { + $installedVersion = $versions[$app]; + if (version_compare($currentVersion, $installedVersion, '>')) { + return true; + } + } + return false; + } + + /** + * Adjust the number of version parts of $version1 to match + * the number of version parts of $version2. + * + * @param string $version1 version to adjust + * @param string $version2 version to take the number of parts from + * @return string shortened $version1 + */ + private static function adjustVersionParts($version1, $version2) { + $version1 = explode('.', $version1); + $version2 = explode('.', $version2); + // reduce $version1 to match the number of parts in $version2 + while (count($version1) > count($version2)) { + array_pop($version1); + } + // if $version1 does not have enough parts, add some + while (count($version1) < count($version2)) { + $version1[] = '0'; + } + return implode('.', $version1); + } + + /** + * Check whether the current ownCloud version matches the given + * application's version requirements. + * + * The comparison is made based on the number of parts that the + * app info version has. For example for ownCloud 6.0.3 if the + * app info version is expecting version 6.0, the comparison is + * made on the first two parts of the ownCloud version. + * This means that it's possible to specify "requiremin" => 6 + * and "requiremax" => 6 and it will still match ownCloud 6.0.3. + * + * @param string $ocVersion ownCloud version to check against + * @param array $appInfo app info (from xml) + * + * @return boolean true if compatible, otherwise false + */ + public static function isAppCompatible($ocVersion, $appInfo) { + $requireMin = ''; + $requireMax = ''; + if (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) { + $requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version']; + } else if (isset($appInfo['requiremin'])) { + $requireMin = $appInfo['requiremin']; + } else if (isset($appInfo['require'])) { + $requireMin = $appInfo['require']; + } + + if (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) { + $requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version']; + } else if (isset($appInfo['requiremax'])) { + $requireMax = $appInfo['requiremax']; + } + + if (is_array($ocVersion)) { + $ocVersion = implode('.', $ocVersion); + } + + if (!empty($requireMin) + && version_compare(self::adjustVersionParts($ocVersion, $requireMin), $requireMin, '<') + ) { + + return false; + } + + if (!empty($requireMax) + && version_compare(self::adjustVersionParts($ocVersion, $requireMax), $requireMax, '>') + ) { + return false; + } + + return true; + } + + /** + * get the installed version of all apps + */ + public static function getAppVersions() { + static $versions; + + if(!$versions) { + $appConfig = \OC::$server->getAppConfig(); + $versions = $appConfig->getValues(false, 'installed_version'); + } + return $versions; + } + + /** + * @param string $app + * @return bool + * @throws Exception if app is not compatible with this version of ownCloud + * @throws Exception if no app-name was specified + */ + public static function installApp($app) { + $l = \OC::$server->getL10N('core'); + $config = \OC::$server->getConfig(); + $ocsClient = new OCSClient( + \OC::$server->getHTTPClientService(), + $config, + \OC::$server->getLogger() + ); + $appData = $ocsClient->getApplication($app, \OCP\Util::getVersion()); + + // check if app is a shipped app or not. OCS apps have an integer as id, shipped apps use a string + if (!is_numeric($app)) { + $shippedVersion = self::getAppVersion($app); + if ($appData && version_compare($shippedVersion, $appData['version'], '<')) { + $app = self::downloadApp($app); + } else { + $app = OC_Installer::installShippedApp($app); + } + } else { + // Maybe the app is already installed - compare the version in this + // case and use the local already installed one. + // FIXME: This is a horrible hack. I feel sad. The god of code cleanness may forgive me. + $internalAppId = self::getInternalAppIdByOcs($app); + if($internalAppId !== false) { + if($appData && version_compare(\OC_App::getAppVersion($internalAppId), $appData['version'], '<')) { + $app = self::downloadApp($app); + } else { + self::enable($internalAppId); + $app = $internalAppId; + } + } else { + $app = self::downloadApp($app); + } + } + + if ($app !== false) { + // check if the app is compatible with this version of ownCloud + $info = self::getAppInfo($app); + if(!is_array($info)) { + throw new \Exception( + $l->t('App "%s" cannot be installed because appinfo file cannot be read.', + [$info['name']] + ) + ); + } + + $version = \OCP\Util::getVersion(); + if (!self::isAppCompatible($version, $info)) { + throw new \Exception( + $l->t('App "%s" cannot be installed because it is not compatible with this version of ownCloud.', + array($info['name']) + ) + ); + } + + // check for required dependencies + $dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l); + $missing = $dependencyAnalyzer->analyze($info); + if (!empty($missing)) { + $missingMsg = join(PHP_EOL, $missing); + throw new \Exception( + $l->t('App "%s" cannot be installed because the following dependencies are not fulfilled: %s', + array($info['name'], $missingMsg) + ) + ); + } + + $config->setAppValue($app, 'enabled', 'yes'); + if (isset($appData['id'])) { + $config->setAppValue($app, 'ocsid', $appData['id']); + } + \OC_Hook::emit('OC_App', 'post_enable', array('app' => $app)); + } else { + throw new \Exception($l->t("No app name specified")); + } + + return $app; + } + + /** + * update the database for the app and call the update script + * + * @param string $appId + * @return bool + */ + public static function updateApp($appId) { + $appPath = self::getAppPath($appId); + if($appPath === false) { + return false; + } + $appData = self::getAppInfo($appId); + self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']); + if (file_exists($appPath . '/appinfo/database.xml')) { + OC_DB::updateDbFromStructure($appPath . '/appinfo/database.xml'); + } + self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']); + self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']); + unset(self::$appVersion[$appId]); + // run upgrade code + if (file_exists($appPath . '/appinfo/update.php')) { + self::loadApp($appId, false); + include $appPath . '/appinfo/update.php'; + } + + //set remote/public handlers + if (array_key_exists('ocsid', $appData)) { + \OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']); + } elseif(\OC::$server->getConfig()->getAppValue($appId, 'ocsid', null) !== null) { + \OC::$server->getConfig()->deleteAppValue($appId, 'ocsid'); + } + foreach ($appData['remote'] as $name => $path) { + \OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path); + } + foreach ($appData['public'] as $name => $path) { + \OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path); + } + + self::setAppTypes($appId); + + $version = \OC_App::getAppVersion($appId); + \OC::$server->getAppConfig()->setValue($appId, 'installed_version', $version); + + return true; + } + + /** + * @param string $appId + * @param string[] $steps + * @throws \OC\NeedsUpdateException + */ + private static function executeRepairSteps($appId, array $steps) { + if (empty($steps)) { + return; + } + // load the app + self::loadApp($appId, false); + + $dispatcher = OC::$server->getEventDispatcher(); + + // load the steps + $r = new Repair([], $dispatcher); + foreach ($steps as $step) { + try { + $r->addStep($step); + } catch (Exception $ex) { + $r->emit('\OC\Repair', 'error', [$ex->getMessage()]); + \OC::$server->getLogger()->logException($ex); + } + } + // run the steps + $r->run(); + } + + /** + * @param string $appId + * @param string[] $steps + */ + private static function setupLiveMigrations($appId, array $steps) { + $queue = \OC::$server->getJobList(); + foreach ($steps as $step) { + $queue->add('OC\Migration\BackgroundRepair', [ + 'app' => $appId, + 'step' => $step]); + } + } + + /** + * @param string $appId + * @return \OC\Files\View|false + */ + public static function getStorage($appId) { + if (OC_App::isEnabled($appId)) { //sanity check + if (OC_User::isLoggedIn()) { + $view = new \OC\Files\View('/' . OC_User::getUser()); + if (!$view->file_exists($appId)) { + $view->mkdir($appId); + } + return new \OC\Files\View('/' . OC_User::getUser() . '/' . $appId); + } else { + \OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ', user not logged in', \OCP\Util::ERROR); + return false; + } + } else { + \OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ' not enabled', \OCP\Util::ERROR); + return false; + } + } + + /** + * parses the app data array and enhanced the 'description' value + * + * @param array $data the app data + * @return array improved app data + */ + public static function parseAppInfo(array $data) { + + // just modify the description if it is available + // otherwise this will create a $data element with an empty 'description' + if (isset($data['description'])) { + if (is_string($data['description'])) { + // sometimes the description contains line breaks and they are then also + // shown in this way in the app management which isn't wanted as HTML + // manages line breaks itself + + // first of all we split on empty lines + $paragraphs = preg_split("!\n[[:space:]]*\n!mu", $data['description']); + + $result = []; + foreach ($paragraphs as $value) { + // replace multiple whitespace (tabs, space, newlines) inside a paragraph + // with a single space - also trims whitespace + $result[] = trim(preg_replace('![[:space:]]+!mu', ' ', $value)); + } + + // join the single paragraphs with a empty line in between + $data['description'] = implode("\n\n", $result); + + } else { + $data['description'] = ''; + } + } + + return $data; + } +} diff --git a/lib/private/legacy/archive.php b/lib/private/legacy/archive.php new file mode 100644 index 00000000000..62512d1448a --- /dev/null +++ b/lib/private/legacy/archive.php @@ -0,0 +1,158 @@ +<?php +/** + * @author Arthur Schiwon <blizzz@owncloud.com> + * @author Bart Visscher <bartv@thisnet.nl> + * @author Christopher Schรคpers <kondou@ts.unde.re> + * @author Felix Moeller <mail@felixmoeller.de> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Jรถrn Friedrich Dreyer <jfd@butonic.de> + * @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> + * + * @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/> + * + */ + +abstract class OC_Archive{ + /** + * Open any of the supported archive types + * + * @param string $path + * @return OC_Archive|void + */ + public static function open($path) { + $mime = \OC::$server->getMimeTypeDetector()->detect($path); + + switch($mime) { + case 'application/zip': + return new OC_Archive_ZIP($path); + case 'application/x-gzip': + return new OC_Archive_TAR($path); + case 'application/x-bzip2': + return new OC_Archive_TAR($path); + } + } + + /** + * @param $source + */ + abstract function __construct($source); + /** + * add an empty folder to the archive + * @param string $path + * @return bool + */ + abstract function addFolder($path); + /** + * add a file to the archive + * @param string $path + * @param string $source either a local file or string data + * @return bool + */ + abstract function addFile($path, $source=''); + /** + * rename a file or folder in the archive + * @param string $source + * @param string $dest + * @return bool + */ + abstract function rename($source, $dest); + /** + * get the uncompressed size of a file in the archive + * @param string $path + * @return int + */ + abstract function filesize($path); + /** + * get the last modified time of a file in the archive + * @param string $path + * @return int + */ + abstract function mtime($path); + /** + * get the files in a folder + * @param string $path + * @return array + */ + abstract function getFolder($path); + /** + * get all files in the archive + * @return array + */ + abstract function getFiles(); + /** + * get the content of a file + * @param string $path + * @return string + */ + abstract function getFile($path); + /** + * extract a single file from the archive + * @param string $path + * @param string $dest + * @return bool + */ + abstract function extractFile($path, $dest); + /** + * extract the archive + * @param string $dest + * @return bool + */ + abstract function extract($dest); + /** + * check if a file or folder exists in the archive + * @param string $path + * @return bool + */ + abstract function fileExists($path); + /** + * remove a file or folder from the archive + * @param string $path + * @return bool + */ + abstract function remove($path); + /** + * get a file handler + * @param string $path + * @param string $mode + * @return resource + */ + abstract function getStream($path, $mode); + /** + * add a folder and all its content + * @param string $path + * @param string $source + * @return boolean|null + */ + function addRecursive($path, $source) { + $dh = opendir($source); + if(is_resource($dh)) { + $this->addFolder($path); + while (($file = readdir($dh)) !== false) { + if($file=='.' or $file=='..') { + continue; + } + if(is_dir($source.'/'.$file)) { + $this->addRecursive($path.'/'.$file, $source.'/'.$file); + }else{ + $this->addFile($path.'/'.$file, $source.'/'.$file); + } + } + } + } +} diff --git a/lib/private/legacy/db.php b/lib/private/legacy/db.php new file mode 100644 index 00000000000..5d612312919 --- /dev/null +++ b/lib/private/legacy/db.php @@ -0,0 +1,258 @@ +<?php +/** + * @author Andreas Fischer <bantu@owncloud.com> + * @author Bart Visscher <bartv@thisnet.nl> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Jรถrn Friedrich Dreyer <jfd@butonic.de> + * @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/> + * + */ + +/** + * This class manages the access to the database. It basically is a wrapper for + * Doctrine with some adaptions. + */ +class OC_DB { + + /** + * get MDB2 schema manager + * + * @return \OC\DB\MDB2SchemaManager + */ + private static function getMDB2SchemaManager() { + return new \OC\DB\MDB2SchemaManager(\OC::$server->getDatabaseConnection()); + } + + /** + * Prepare a SQL query + * @param string $query Query string + * @param int $limit + * @param int $offset + * @param bool $isManipulation + * @throws \OC\DatabaseException + * @return OC_DB_StatementWrapper prepared SQL query + * + * SQL query via Doctrine prepare(), needs to be execute()'d! + */ + static public function prepare( $query , $limit = null, $offset = null, $isManipulation = null) { + $connection = \OC::$server->getDatabaseConnection(); + + if ($isManipulation === null) { + //try to guess, so we return the number of rows on manipulations + $isManipulation = self::isManipulation($query); + } + + // return the result + try { + $result =$connection->prepare($query, $limit, $offset); + } catch (\Doctrine\DBAL\DBALException $e) { + throw new \OC\DatabaseException($e->getMessage(), $query); + } + // differentiate between query and manipulation + $result = new OC_DB_StatementWrapper($result, $isManipulation); + return $result; + } + + /** + * tries to guess the type of statement based on the first 10 characters + * the current check allows some whitespace but does not work with IF EXISTS or other more complex statements + * + * @param string $sql + * @return bool + */ + static public function isManipulation( $sql ) { + $selectOccurrence = stripos($sql, 'SELECT'); + if ($selectOccurrence !== false && $selectOccurrence < 10) { + return false; + } + $insertOccurrence = stripos($sql, 'INSERT'); + if ($insertOccurrence !== false && $insertOccurrence < 10) { + return true; + } + $updateOccurrence = stripos($sql, 'UPDATE'); + if ($updateOccurrence !== false && $updateOccurrence < 10) { + return true; + } + $deleteOccurrence = stripos($sql, 'DELETE'); + if ($deleteOccurrence !== false && $deleteOccurrence < 10) { + return true; + } + return false; + } + + /** + * execute a prepared statement, on error write log and throw exception + * @param mixed $stmt OC_DB_StatementWrapper, + * an array with 'sql' and optionally 'limit' and 'offset' keys + * .. or a simple sql query string + * @param array $parameters + * @return OC_DB_StatementWrapper + * @throws \OC\DatabaseException + */ + static public function executeAudited( $stmt, array $parameters = null) { + if (is_string($stmt)) { + // convert to an array with 'sql' + if (stripos($stmt, 'LIMIT') !== false) { //OFFSET requires LIMIT, so we only need to check for LIMIT + // TODO try to convert LIMIT OFFSET notation to parameters + $message = 'LIMIT and OFFSET are forbidden for portability reasons,' + . ' pass an array with \'limit\' and \'offset\' instead'; + throw new \OC\DatabaseException($message); + } + $stmt = array('sql' => $stmt, 'limit' => null, 'offset' => null); + } + if (is_array($stmt)) { + // convert to prepared statement + if ( ! array_key_exists('sql', $stmt) ) { + $message = 'statement array must at least contain key \'sql\''; + throw new \OC\DatabaseException($message); + } + if ( ! array_key_exists('limit', $stmt) ) { + $stmt['limit'] = null; + } + if ( ! array_key_exists('limit', $stmt) ) { + $stmt['offset'] = null; + } + $stmt = self::prepare($stmt['sql'], $stmt['limit'], $stmt['offset']); + } + self::raiseExceptionOnError($stmt, 'Could not prepare statement'); + if ($stmt instanceof OC_DB_StatementWrapper) { + $result = $stmt->execute($parameters); + self::raiseExceptionOnError($result, 'Could not execute statement'); + } else { + if (is_object($stmt)) { + $message = 'Expected a prepared statement or array got ' . get_class($stmt); + } else { + $message = 'Expected a prepared statement or array got ' . gettype($stmt); + } + throw new \OC\DatabaseException($message); + } + return $result; + } + + /** + * saves database schema to xml file + * @param string $file name of file + * @param int $mode + * @return bool + * + * TODO: write more documentation + */ + public static function getDbStructure($file) { + $schemaManager = self::getMDB2SchemaManager(); + return $schemaManager->getDbStructure($file); + } + + /** + * Creates tables from XML file + * @param string $file file to read structure from + * @return bool + * + * TODO: write more documentation + */ + public static function createDbFromStructure( $file ) { + $schemaManager = self::getMDB2SchemaManager(); + $result = $schemaManager->createDbFromStructure($file); + return $result; + } + + /** + * update the database schema + * @param string $file file to read structure from + * @throws Exception + * @return string|boolean + */ + public static function updateDbFromStructure($file) { + $schemaManager = self::getMDB2SchemaManager(); + try { + $result = $schemaManager->updateDbFromStructure($file); + } catch (Exception $e) { + \OCP\Util::writeLog('core', 'Failed to update database structure ('.$e.')', \OCP\Util::FATAL); + throw $e; + } + return $result; + } + + /** + * simulate the database schema update + * @param string $file file to read structure from + * @throws Exception + * @return string|boolean + */ + public static function simulateUpdateDbFromStructure($file) { + $schemaManager = self::getMDB2SchemaManager(); + try { + $result = $schemaManager->simulateUpdateDbFromStructure($file); + } catch (Exception $e) { + \OCP\Util::writeLog('core', 'Simulated database structure update failed ('.$e.')', \OCP\Util::FATAL); + throw $e; + } + return $result; + } + + /** + * remove all tables defined in a database structure xml file + * @param string $file the xml file describing the tables + */ + public static function removeDBStructure($file) { + $schemaManager = self::getMDB2SchemaManager(); + $schemaManager->removeDBStructure($file); + } + + /** + * check if a result is an error and throws an exception, works with \Doctrine\DBAL\DBALException + * @param mixed $result + * @param string $message + * @return void + * @throws \OC\DatabaseException + */ + public static function raiseExceptionOnError($result, $message = null) { + if($result === false) { + if ($message === null) { + $message = self::getErrorMessage(); + } else { + $message .= ', Root cause:' . self::getErrorMessage(); + } + throw new \OC\DatabaseException($message, \OC::$server->getDatabaseConnection()->errorCode()); + } + } + + /** + * returns the error code and message as a string for logging + * works with DoctrineException + * @return string + */ + public static function getErrorMessage() { + $connection = \OC::$server->getDatabaseConnection(); + return $connection->getError(); + } + + /** + * Checks if a table exists in the database - the database prefix will be prepended + * + * @param string $table + * @return bool + * @throws \OC\DatabaseException + */ + public static function tableExists($table) { + $connection = \OC::$server->getDatabaseConnection(); + return $connection->tableExists($table); + } +} diff --git a/lib/private/legacy/defaults.php b/lib/private/legacy/defaults.php new file mode 100644 index 00000000000..43e8c8082cc --- /dev/null +++ b/lib/private/legacy/defaults.php @@ -0,0 +1,286 @@ +<?php +/** + * @author Bjรถrn Schieรle <schiessle@owncloud.com> + * @author Jan-Christoph Borchardt <hey@jancborchardt.net> + * @author Jรถrn Friedrich Dreyer <jfd@butonic.de> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Pascal de Bruijn <pmjdebruijn@pcode.nl> + * @author Robin Appelman <icewind@owncloud.com> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author scolebrook <scolebrook@mac.com> + * @author Thomas Mรผller <thomas.mueller@tmit.eu> + * @author Volkan Gezer <volkangezer@gmail.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/> + * + */ +class OC_Defaults { + + private $theme; + private $l; + + private $defaultEntity; + private $defaultName; + private $defaultTitle; + private $defaultBaseUrl; + private $defaultSyncClientUrl; + private $defaultiOSClientUrl; + private $defaultiTunesAppId; + private $defaultAndroidClientUrl; + private $defaultDocBaseUrl; + private $defaultDocVersion; + private $defaultSlogan; + private $defaultLogoClaim; + private $defaultMailHeaderColor; + + function __construct() { + $this->l = \OC::$server->getL10N('lib'); + $version = \OCP\Util::getVersion(); + + $this->defaultEntity = 'ownCloud'; /* e.g. company name, used for footers and copyright notices */ + $this->defaultName = 'ownCloud'; /* short name, used when referring to the software */ + $this->defaultTitle = 'ownCloud'; /* can be a longer name, for titles */ + $this->defaultBaseUrl = 'https://owncloud.org'; + $this->defaultSyncClientUrl = 'https://owncloud.org/sync-clients/'; + $this->defaultiOSClientUrl = 'https://itunes.apple.com/us/app/owncloud/id543672169?mt=8'; + $this->defaultiTunesAppId = '543672169'; + $this->defaultAndroidClientUrl = 'https://play.google.com/store/apps/details?id=com.owncloud.android'; + $this->defaultDocBaseUrl = 'https://doc.owncloud.org'; + $this->defaultDocVersion = $version[0] . '.' . $version[1]; // used to generate doc links + $this->defaultSlogan = $this->l->t('web services under your control'); + $this->defaultLogoClaim = ''; + $this->defaultMailHeaderColor = '#1d2d44'; /* header color of mail notifications */ + + $themePath = OC::$SERVERROOT . '/themes/' . OC_Util::getTheme() . '/defaults.php'; + if (file_exists($themePath)) { + // prevent defaults.php from printing output + ob_start(); + require_once $themePath; + ob_end_clean(); + if (class_exists('OC_Theme')) { + $this->theme = new OC_Theme(); + } + } + } + + /** + * @param string $method + */ + private function themeExist($method) { + if (isset($this->theme) && method_exists($this->theme, $method)) { + return true; + } + return false; + } + + /** + * Returns the base URL + * @return string URL + */ + public function getBaseUrl() { + if ($this->themeExist('getBaseUrl')) { + return $this->theme->getBaseUrl(); + } else { + return $this->defaultBaseUrl; + } + } + + /** + * Returns the URL where the sync clients are listed + * @return string URL + */ + public function getSyncClientUrl() { + if ($this->themeExist('getSyncClientUrl')) { + return $this->theme->getSyncClientUrl(); + } else { + return $this->defaultSyncClientUrl; + } + } + + /** + * Returns the URL to the App Store for the iOS Client + * @return string URL + */ + public function getiOSClientUrl() { + if ($this->themeExist('getiOSClientUrl')) { + return $this->theme->getiOSClientUrl(); + } else { + return $this->defaultiOSClientUrl; + } + } + + /** + * Returns the AppId for the App Store for the iOS Client + * @return string AppId + */ + public function getiTunesAppId() { + if ($this->themeExist('getiTunesAppId')) { + return $this->theme->getiTunesAppId(); + } else { + return $this->defaultiTunesAppId; + } + } + + /** + * Returns the URL to Google Play for the Android Client + * @return string URL + */ + public function getAndroidClientUrl() { + if ($this->themeExist('getAndroidClientUrl')) { + return $this->theme->getAndroidClientUrl(); + } else { + return $this->defaultAndroidClientUrl; + } + } + + /** + * Returns the documentation URL + * @return string URL + */ + public function getDocBaseUrl() { + if ($this->themeExist('getDocBaseUrl')) { + return $this->theme->getDocBaseUrl(); + } else { + return $this->defaultDocBaseUrl; + } + } + + /** + * Returns the title + * @return string title + */ + public function getTitle() { + if ($this->themeExist('getTitle')) { + return $this->theme->getTitle(); + } else { + return $this->defaultTitle; + } + } + + /** + * Returns the short name of the software + * @return string title + */ + public function getName() { + if ($this->themeExist('getName')) { + return $this->theme->getName(); + } else { + return $this->defaultName; + } + } + + /** + * Returns the short name of the software containing HTML strings + * @return string title + */ + public function getHTMLName() { + if ($this->themeExist('getHTMLName')) { + return $this->theme->getHTMLName(); + } else { + return $this->defaultName; + } + } + + /** + * Returns entity (e.g. company name) - used for footer, copyright + * @return string entity name + */ + public function getEntity() { + if ($this->themeExist('getEntity')) { + return $this->theme->getEntity(); + } else { + return $this->defaultEntity; + } + } + + /** + * Returns slogan + * @return string slogan + */ + public function getSlogan() { + if ($this->themeExist('getSlogan')) { + return $this->theme->getSlogan(); + } else { + return $this->defaultSlogan; + } + } + + /** + * Returns logo claim + * @return string logo claim + */ + public function getLogoClaim() { + if ($this->themeExist('getLogoClaim')) { + return $this->theme->getLogoClaim(); + } else { + return $this->defaultLogoClaim; + } + } + + /** + * Returns short version of the footer + * @return string short footer + */ + public function getShortFooter() { + if ($this->themeExist('getShortFooter')) { + $footer = $this->theme->getShortFooter(); + } else { + $footer = '<a href="'. $this->getBaseUrl() . '" target="_blank"' . + ' rel="noreferrer">' .$this->getEntity() . '</a>'. + ' โ ' . $this->getSlogan(); + } + + return $footer; + } + + /** + * Returns long version of the footer + * @return string long footer + */ + public function getLongFooter() { + if ($this->themeExist('getLongFooter')) { + $footer = $this->theme->getLongFooter(); + } else { + $footer = $this->getShortFooter(); + } + + return $footer; + } + + /** + * @param string $key + */ + public function buildDocLinkToKey($key) { + if ($this->themeExist('buildDocLinkToKey')) { + return $this->theme->buildDocLinkToKey($key); + } + return $this->getDocBaseUrl() . '/server/' . $this->defaultDocVersion . '/go.php?to=' . $key; + } + + /** + * Returns mail header color + * @return string + */ + public function getMailHeaderColor() { + if ($this->themeExist('getMailHeaderColor')) { + return $this->theme->getMailHeaderColor(); + } else { + return $this->defaultMailHeaderColor; + } + } + +} diff --git a/lib/private/legacy/eventsource.php b/lib/private/legacy/eventsource.php new file mode 100644 index 00000000000..f567d1e6ca5 --- /dev/null +++ b/lib/private/legacy/eventsource.php @@ -0,0 +1,125 @@ +<?php +/** + * @author Bart Visscher <bartv@thisnet.nl> + * @author Felix Moeller <mail@felixmoeller.de> + * @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/> + * + */ + +/** + * wrapper for server side events (http://en.wikipedia.org/wiki/Server-sent_events) + * includes a fallback for older browsers and IE + * + * use server side events with caution, to many open requests can hang the server + */ +class OC_EventSource implements \OCP\IEventSource { + /** + * @var bool + */ + private $fallback; + + /** + * @var int + */ + private $fallBackId = 0; + + /** + * @var bool + */ + private $started = false; + + protected function init() { + if ($this->started) { + return; + } + $this->started = true; + + // prevent php output buffering, caching and nginx buffering + OC_Util::obEnd(); + header('Cache-Control: no-cache'); + header('X-Accel-Buffering: no'); + $this->fallback = isset($_GET['fallback']) and $_GET['fallback'] == 'true'; + if ($this->fallback) { + $this->fallBackId = (int)$_GET['fallback_id']; + /** + * FIXME: The default content-security-policy of ownCloud forbids inline + * JavaScript for security reasons. IE starting on Windows 10 will + * however also obey the CSP which will break the event source fallback. + * + * As a workaround thus we set a custom policy which allows the execution + * of inline JavaScript. + * + * @link https://github.com/owncloud/core/issues/14286 + */ + header("Content-Security-Policy: default-src 'none'; script-src 'unsafe-inline'"); + header("Content-Type: text/html"); + echo str_repeat('<span></span>' . PHP_EOL, 10); //dummy data to keep IE happy + } else { + header("Content-Type: text/event-stream"); + } + if (!(\OC::$server->getRequest()->passesCSRFCheck())) { + $this->send('error', 'Possible CSRF attack. Connection will be closed.'); + $this->close(); + exit(); + } + flush(); + } + + /** + * send a message to the client + * + * @param string $type + * @param mixed $data + * + * @throws \BadMethodCallException + * if only one parameter is given, a typeless message will be send with that parameter as data + */ + public function send($type, $data = null) { + if ($data and !preg_match('/^[A-Za-z0-9_]+$/', $type)) { + throw new BadMethodCallException('Type needs to be alphanumeric ('. $type .')'); + } + $this->init(); + if (is_null($data)) { + $data = $type; + $type = null; + } + if ($this->fallback) { + $response = '<script type="text/javascript">window.parent.OC.EventSource.fallBackCallBack(' + . $this->fallBackId . ',"' . $type . '",' . OCP\JSON::encode($data) . ')</script>' . PHP_EOL; + echo $response; + } else { + if ($type) { + echo 'event: ' . $type . PHP_EOL; + } + echo 'data: ' . OCP\JSON::encode($data) . PHP_EOL; + } + echo PHP_EOL; + flush(); + } + + /** + * close the connection of the event source + */ + public function close() { + $this->send('__internal__', 'close'); //server side closing can be an issue, let the client do it + } +} diff --git a/lib/private/legacy/filechunking.php b/lib/private/legacy/filechunking.php new file mode 100644 index 00000000000..f2cef275458 --- /dev/null +++ b/lib/private/legacy/filechunking.php @@ -0,0 +1,175 @@ +<?php +/** + * @author Bart Visscher <bartv@thisnet.nl> + * @author Felix Moeller <mail@felixmoeller.de> + * @author Jรถrn Friedrich Dreyer <jfd@butonic.de> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Thomas Mรผller <thomas.mueller@tmit.eu> + * @author Thomas Tanghus <thomas@tanghus.net> + * @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/> + * + */ + + +class OC_FileChunking { + protected $info; + protected $cache; + + static public function decodeName($name) { + preg_match('/(?P<name>.*)-chunking-(?P<transferid>\d+)-(?P<chunkcount>\d+)-(?P<index>\d+)/', $name, $matches); + return $matches; + } + + /** + * @param string[] $info + */ + public function __construct($info) { + $this->info = $info; + } + + public function getPrefix() { + $name = $this->info['name']; + $transferid = $this->info['transferid']; + + return $name.'-chunking-'.$transferid.'-'; + } + + protected function getCache() { + if (!isset($this->cache)) { + $this->cache = new \OC\Cache\File(); + } + return $this->cache; + } + + /** + * Stores the given $data under the given $key - the number of stored bytes is returned + * + * @param string $index + * @param resource $data + * @return int + */ + public function store($index, $data) { + $cache = $this->getCache(); + $name = $this->getPrefix().$index; + $cache->set($name, $data); + + return $cache->size($name); + } + + public function isComplete() { + $prefix = $this->getPrefix(); + $cache = $this->getCache(); + $chunkcount = (int)$this->info['chunkcount']; + + for($i=($chunkcount-1); $i >= 0; $i--) { + if (!$cache->hasKey($prefix.$i)) { + return false; + } + } + + return true; + } + + /** + * Assembles the chunks into the file specified by the path. + * Chunks are deleted afterwards. + * + * @param resource $f target path + * + * @return integer assembled file size + * + * @throws \OC\InsufficientStorageException when file could not be fully + * assembled due to lack of free space + */ + public function assemble($f) { + $cache = $this->getCache(); + $prefix = $this->getPrefix(); + $count = 0; + for ($i = 0; $i < $this->info['chunkcount']; $i++) { + $chunk = $cache->get($prefix.$i); + // remove after reading to directly save space + $cache->remove($prefix.$i); + $count += fwrite($f, $chunk); + // let php release the memory to work around memory exhausted error with php 5.6 + $chunk = null; + } + + return $count; + } + + /** + * Returns the size of the chunks already present + * @return integer size in bytes + */ + public function getCurrentSize() { + $cache = $this->getCache(); + $prefix = $this->getPrefix(); + $total = 0; + for ($i = 0; $i < $this->info['chunkcount']; $i++) { + $total += $cache->size($prefix.$i); + } + return $total; + } + + /** + * Removes all chunks which belong to this transmission + */ + public function cleanup() { + $cache = $this->getCache(); + $prefix = $this->getPrefix(); + for($i=0; $i < $this->info['chunkcount']; $i++) { + $cache->remove($prefix.$i); + } + } + + /** + * Removes one specific chunk + * @param string $index + */ + public function remove($index) { + $cache = $this->getCache(); + $prefix = $this->getPrefix(); + $cache->remove($prefix.$index); + } + + /** + * Assembles the chunks into the file specified by the path. + * Also triggers the relevant hooks and proxies. + * + * @param \OC\Files\Storage\Storage $storage storage + * @param string $path target path relative to the storage + * @return bool true on success or false if file could not be created + * + * @throws \OC\ServerNotAvailableException + */ + public function file_assemble($storage, $path) { + // use file_put_contents as method because that best matches what this function does + if (\OC\Files\Filesystem::isValidPath($path)) { + $target = $storage->fopen($path, 'w'); + if ($target) { + $count = $this->assemble($target); + fclose($target); + return $count > 0; + } else { + return false; + } + } + return false; + } +} diff --git a/lib/private/legacy/files.php b/lib/private/legacy/files.php new file mode 100644 index 00000000000..9b6a1a4465f --- /dev/null +++ b/lib/private/legacy/files.php @@ -0,0 +1,316 @@ +<?php +/** + * @author Arthur Schiwon <blizzz@owncloud.com> + * @author Bart Visscher <bartv@thisnet.nl> + * @author Bjรถrn Schieรle <schiessle@owncloud.com> + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Frank Karlitschek <frank@owncloud.org> + * @author Jakob Sack <mail@jakobsack.de> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Jรถrn Friedrich Dreyer <jfd@butonic.de> + * @author Michael Gapczynski <GapczynskiM@gmail.com> + * @author Nicolai Ehemann <en@enlightened.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author Thibaut GRIDEL <tgridel@free.fr> + * @author Thomas Mรผller <thomas.mueller@tmit.eu> + * @author Victor Dubiniuk <dubiniuk@owncloud.com> + * @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/> + * + */ + +use OC\Files\View; +use OC\Streamer; +use OCP\Lock\ILockingProvider; + +/** + * Class for file server access + * + */ +class OC_Files { + const FILE = 1; + const ZIP_FILES = 2; + const ZIP_DIR = 3; + + const UPLOAD_MIN_LIMIT_BYTES = 1048576; // 1 MiB + + /** + * @param string $filename + * @param string $name + */ + private static function sendHeaders($filename, $name) { + OC_Response::setContentDispositionHeader($name, 'attachment'); + header('Content-Transfer-Encoding: binary'); + OC_Response::disableCaching(); + $fileSize = \OC\Files\Filesystem::filesize($filename); + $type = \OC::$server->getMimeTypeDetector()->getSecureMimeType(\OC\Files\Filesystem::getMimeType($filename)); + header('Content-Type: '.$type); + if ($fileSize > -1) { + OC_Response::setContentLengthHeader($fileSize); + } + } + + /** + * return the content of a file or return a zip file containing multiple files + * + * @param string $dir + * @param string $files ; separated list of files to download + * @param boolean $onlyHeader ; boolean to only send header of the request + */ + public static function get($dir, $files, $onlyHeader = false) { + + $view = \OC\Files\Filesystem::getView(); + $getType = self::FILE; + $filename = $dir; + try { + + if (is_array($files) && count($files) === 1) { + $files = $files[0]; + } + + if (!is_array($files)) { + $filename = $dir . '/' . $files; + if (!$view->is_dir($filename)) { + self::getSingleFile($view, $dir, $files, $onlyHeader); + return; + } + } + + $name = 'download'; + if (is_array($files)) { + $getType = self::ZIP_FILES; + $basename = basename($dir); + if ($basename) { + $name = $basename; + } + + $filename = $dir . '/' . $name; + } else { + $filename = $dir . '/' . $files; + $getType = self::ZIP_DIR; + // downloading root ? + if ($files !== '') { + $name = $files; + } + } + + $streamer = new Streamer(); + OC_Util::obEnd(); + + self::lockFiles($view, $dir, $files); + + $streamer->sendHeaders($name); + $executionTime = intval(OC::$server->getIniWrapper()->getNumeric('max_execution_time')); + set_time_limit(0); + if ($getType === self::ZIP_FILES) { + foreach ($files as $file) { + $file = $dir . '/' . $file; + if (\OC\Files\Filesystem::is_file($file)) { + $fileSize = \OC\Files\Filesystem::filesize($file); + $fh = \OC\Files\Filesystem::fopen($file, 'r'); + $streamer->addFileFromStream($fh, basename($file), $fileSize); + fclose($fh); + } elseif (\OC\Files\Filesystem::is_dir($file)) { + $streamer->addDirRecursive($file); + } + } + } elseif ($getType === self::ZIP_DIR) { + $file = $dir . '/' . $files; + $streamer->addDirRecursive($file); + } + $streamer->finalize(); + set_time_limit($executionTime); + self::unlockAllTheFiles($dir, $files, $getType, $view, $filename); + } catch (\OCP\Lock\LockedException $ex) { + self::unlockAllTheFiles($dir, $files, $getType, $view, $filename); + OC::$server->getLogger()->logException($ex); + $l = \OC::$server->getL10N('core'); + $hint = method_exists($ex, 'getHint') ? $ex->getHint() : ''; + \OC_Template::printErrorPage($l->t('File is currently busy, please try again later'), $hint); + } catch (\OCP\Files\ForbiddenException $ex) { + self::unlockAllTheFiles($dir, $files, $getType, $view, $filename); + OC::$server->getLogger()->logException($ex); + $l = \OC::$server->getL10N('core'); + \OC_Template::printErrorPage($l->t('Can\'t read file'), $ex->getMessage()); + } catch (\Exception $ex) { + self::unlockAllTheFiles($dir, $files, $getType, $view, $filename); + OC::$server->getLogger()->logException($ex); + $l = \OC::$server->getL10N('core'); + $hint = method_exists($ex, 'getHint') ? $ex->getHint() : ''; + \OC_Template::printErrorPage($l->t('Can\'t read file'), $hint); + } + } + + /** + * @param View $view + * @param string $name + * @param string $dir + * @param boolean $onlyHeader + */ + private static function getSingleFile($view, $dir, $name, $onlyHeader) { + $filename = $dir . '/' . $name; + OC_Util::obEnd(); + $view->lockFile($filename, ILockingProvider::LOCK_SHARED); + + if (\OC\Files\Filesystem::isReadable($filename)) { + self::sendHeaders($filename, $name); + } elseif (!\OC\Files\Filesystem::file_exists($filename)) { + header("HTTP/1.0 404 Not Found"); + $tmpl = new OC_Template('', '404', 'guest'); + $tmpl->printPage(); + exit(); + } else { + header("HTTP/1.0 403 Forbidden"); + die('403 Forbidden'); + } + if ($onlyHeader) { + return; + } + $view->readfile($filename); + } + + /** + * @param View $view + * @param string $dir + * @param string[]|string $files + */ + public static function lockFiles($view, $dir, $files) { + if (!is_array($files)) { + $file = $dir . '/' . $files; + $files = [$file]; + } + foreach ($files as $file) { + $file = $dir . '/' . $file; + $view->lockFile($file, ILockingProvider::LOCK_SHARED); + if ($view->is_dir($file)) { + $contents = $view->getDirectoryContent($file); + $contents = array_map(function($fileInfo) use ($file) { + /** @var \OCP\Files\FileInfo $fileInfo */ + return $file . '/' . $fileInfo->getName(); + }, $contents); + self::lockFiles($view, $dir, $contents); + } + } + } + + /** + * set the maximum upload size limit for apache hosts using .htaccess + * + * @param int $size file size in bytes + * @param array $files override '.htaccess' and '.user.ini' locations + * @return bool false on failure, size on success + */ + public static function setUploadLimit($size, $files = []) { + //don't allow user to break his config + $size = intval($size); + if ($size < self::UPLOAD_MIN_LIMIT_BYTES) { + return false; + } + $size = OC_Helper::phpFileSize($size); + + $phpValueKeys = array( + 'upload_max_filesize', + 'post_max_size' + ); + + // default locations if not overridden by $files + $files = array_merge([ + '.htaccess' => OC::$SERVERROOT . '/.htaccess', + '.user.ini' => OC::$SERVERROOT . '/.user.ini' + ], $files); + + $updateFiles = [ + $files['.htaccess'] => [ + 'pattern' => '/php_value %1$s (\S)*/', + 'setting' => 'php_value %1$s %2$s' + ], + $files['.user.ini'] => [ + 'pattern' => '/%1$s=(\S)*/', + 'setting' => '%1$s=%2$s' + ] + ]; + + $success = true; + + foreach ($updateFiles as $filename => $patternMap) { + // suppress warnings from fopen() + $handle = @fopen($filename, 'r+'); + if (!$handle) { + \OCP\Util::writeLog('files', + 'Can\'t write upload limit to ' . $filename . '. Please check the file permissions', + \OCP\Util::WARN); + $success = false; + continue; // try to update as many files as possible + } + + $content = ''; + while (!feof($handle)) { + $content .= fread($handle, 1000); + } + + foreach ($phpValueKeys as $key) { + $pattern = vsprintf($patternMap['pattern'], [$key]); + $setting = vsprintf($patternMap['setting'], [$key, $size]); + $hasReplaced = 0; + $newContent = preg_replace($pattern, $setting, $content, 2, $hasReplaced); + if ($newContent !== null) { + $content = $newContent; + } + if ($hasReplaced === 0) { + $content .= "\n" . $setting; + } + } + + // write file back + ftruncate($handle, 0); + rewind($handle); + fwrite($handle, $content); + + fclose($handle); + } + + if ($success) { + return OC_Helper::computerFileSize($size); + } + return false; + } + + /** + * @param string $dir + * @param $files + * @param integer $getType + * @param View $view + * @param string $filename + */ + private static function unlockAllTheFiles($dir, $files, $getType, $view, $filename) { + if ($getType === self::FILE) { + $view->unlockFile($filename, ILockingProvider::LOCK_SHARED); + } + if ($getType === self::ZIP_FILES) { + foreach ($files as $file) { + $file = $dir . '/' . $file; + $view->unlockFile($file, ILockingProvider::LOCK_SHARED); + } + } + if ($getType === self::ZIP_DIR) { + $file = $dir . '/' . $files; + $view->unlockFile($file, ILockingProvider::LOCK_SHARED); + } + } + +} diff --git a/lib/private/legacy/group.php b/lib/private/legacy/group.php new file mode 100644 index 00000000000..f1b84069a38 --- /dev/null +++ b/lib/private/legacy/group.php @@ -0,0 +1,300 @@ +<?php +/** + * @author Arthur Schiwon <blizzz@owncloud.com> + * @author Bart Visscher <bartv@thisnet.nl> + * @author Bjรถrn Schieรle <schiessle@owncloud.com> + * @author Georg Ehrke <georg@owncloud.com> + * @author goodkiller <markopraakli@gmail.com> + * @author Jakob Sack <mail@jakobsack.de> + * @author Lukas Reschke <lukas@owncloud.com> + * @author macjohnny <estebanmarin@gmx.ch> + * @author Michael Gapczynski <GapczynskiM@gmail.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Qingping Hou <dave2008713@gmail.com> + * @author Robin Appelman <icewind@owncloud.com> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @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/> + * + */ + +/** + * This class provides all methods needed for managing groups. + * + * Note that &run is deprecated and won't work anymore. + * Hooks provided: + * pre_createGroup(&run, gid) + * post_createGroup(gid) + * pre_deleteGroup(&run, gid) + * post_deleteGroup(gid) + * pre_addToGroup(&run, uid, gid) + * post_addToGroup(uid, gid) + * pre_removeFromGroup(&run, uid, gid) + * post_removeFromGroup(uid, gid) + */ +class OC_Group { + + /** + * @return \OC\Group\Manager + * @deprecated Use \OC::$server->getGroupManager(); + */ + public static function getManager() { + return \OC::$server->getGroupManager(); + } + + /** + * @return \OC\User\Manager + * @deprecated Use \OC::$server->getUserManager() + */ + private static function getUserManager() { + return \OC::$server->getUserManager(); + } + + /** + * set the group backend + * @param \OC_Group_Backend $backend The backend to use for user management + * @return bool + */ + public static function useBackend($backend) { + self::getManager()->addBackend($backend); + return true; + } + + /** + * remove all used backends + */ + public static function clearBackends() { + self::getManager()->clearBackends(); + } + + /** + * Try to create a new group + * @param string $gid The name of the group to create + * @return bool + * + * Tries to create a new group. If the group name already exists, false will + * be returned. Basic checking of Group name + * @deprecated Use \OC::$server->getGroupManager()->createGroup() instead + */ + public static function createGroup($gid) { + if (self::getManager()->createGroup($gid)) { + return true; + } else { + return false; + } + } + + /** + * delete a group + * @param string $gid gid of the group to delete + * @return bool + * + * Deletes a group and removes it from the group_user-table + * @deprecated Use \OC::$server->getGroupManager()->delete() instead + */ + public static function deleteGroup($gid) { + $group = self::getManager()->get($gid); + if ($group) { + if ($group->delete()) { + return true; + } + } + return false; + } + + /** + * is user in group? + * @param string $uid uid of the user + * @param string $gid gid of the group + * @return bool + * + * Checks whether the user is member of a group or not. + * @deprecated Use \OC::$server->getGroupManager->inGroup($user); + */ + public static function inGroup($uid, $gid) { + $group = self::getManager()->get($gid); + $user = self::getUserManager()->get($uid); + if ($group and $user) { + return $group->inGroup($user); + } + return false; + } + + /** + * Add a user to a group + * @param string $uid Name of the user to add to group + * @param string $gid Name of the group in which add the user + * @return bool + * + * Adds a user to a group. + * @deprecated Use \OC::$server->getGroupManager->addUser(); + */ + public static function addToGroup($uid, $gid) { + $group = self::getManager()->get($gid); + $user = self::getUserManager()->get($uid); + if ($group and $user) { + $group->addUser($user); + return true; + } else { + return false; + } + } + + /** + * Removes a user from a group + * @param string $uid Name of the user to remove from group + * @param string $gid Name of the group from which remove the user + * @return bool + * + * removes the user from a group. + */ + public static function removeFromGroup($uid, $gid) { + $group = self::getManager()->get($gid); + $user = self::getUserManager()->get($uid); + if ($group and $user) { + OC_Hook::emit("OC_Group", "pre_removeFromGroup", array("run" => true, "uid" => $uid, "gid" => $gid)); + $group->removeUser($user); + OC_Hook::emit("OC_User", "post_removeFromGroup", array("uid" => $uid, "gid" => $gid)); + return true; + } else { + return false; + } + } + + /** + * Get all groups a user belongs to + * @param string $uid Name of the user + * @return array an array of group names + * + * This function fetches all groups a user belongs to. It does not check + * if the user exists at all. + * @deprecated Use \OC::$server->getGroupManager->getUserGroupIds($user) + */ + public static function getUserGroups($uid) { + $user = self::getUserManager()->get($uid); + if ($user) { + return self::getManager()->getUserGroupIds($user); + } else { + return array(); + } + } + + /** + * get a list of all groups + * @param string $search + * @param int|null $limit + * @param int|null $offset + * @return array an array of group names + * + * Returns a list with all groups + */ + public static function getGroups($search = '', $limit = null, $offset = null) { + $groups = self::getManager()->search($search, $limit, $offset); + $groupIds = array(); + foreach ($groups as $group) { + $groupIds[] = $group->getGID(); + } + return $groupIds; + } + + /** + * check if a group exists + * + * @param string $gid + * @return bool + * @deprecated Use \OC::$server->getGroupManager->groupExists($gid) + */ + public static function groupExists($gid) { + return self::getManager()->groupExists($gid); + } + + /** + * get a list of all users in a group + * @param string $gid + * @param string $search + * @param int $limit + * @param int $offset + * @return array an array of user ids + */ + public static function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) { + $group = self::getManager()->get($gid); + if ($group) { + $users = $group->searchUsers($search, $limit, $offset); + $userIds = array(); + foreach ($users as $user) { + $userIds[] = $user->getUID(); + } + return $userIds; + } else { + return array(); + } + } + + /** + * get a list of all users in several groups + * @param string[] $gids + * @param string $search + * @param int $limit + * @param int $offset + * @return array an array of user ids + */ + public static function usersInGroups($gids, $search = '', $limit = -1, $offset = 0) { + $users = array(); + foreach ($gids as $gid) { + // TODO Need to apply limits to groups as total + $users = array_merge(array_diff(self::usersInGroup($gid, $search, $limit, $offset), $users), $users); + } + return $users; + } + + /** + * get a list of all display names in a group + * @param string $gid + * @param string $search + * @param int $limit + * @param int $offset + * @return array an array of display names (value) and user ids(key) + * @deprecated Use \OC::$server->getGroupManager->displayNamesInGroup($gid, $search, $limit, $offset) + */ + public static function displayNamesInGroup($gid, $search = '', $limit = -1, $offset = 0) { + return self::getManager()->displayNamesInGroup($gid, $search, $limit, $offset); + } + + /** + * get a list of all display names in several groups + * @param array $gids + * @param string $search + * @param int $limit + * @param int $offset + * @return array an array of display names (Key) user ids (value) + */ + public static function displayNamesInGroups($gids, $search = '', $limit = -1, $offset = 0) { + $displayNames = array(); + foreach ($gids as $gid) { + // TODO Need to apply limits to groups as total + $diff = array_diff( + self::displayNamesInGroup($gid, $search, $limit, $offset), + $displayNames + ); + if ($diff) { + // A fix for LDAP users. array_merge loses keys... + $displayNames = $diff + $displayNames; + } + } + return $displayNames; + } +} diff --git a/lib/private/legacy/helper.php b/lib/private/legacy/helper.php new file mode 100644 index 00000000000..e6aaed0fd15 --- /dev/null +++ b/lib/private/legacy/helper.php @@ -0,0 +1,703 @@ +<?php +/** + * @author Arthur Schiwon <blizzz@owncloud.com> + * @author Bart Visscher <bartv@thisnet.nl> + * @author Bjรถrn Schieรle <schiessle@owncloud.com> + * @author Christopher Schรคpers <kondou@ts.unde.re> + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Fabian Henze <flyser42@gmx.de> + * @author Felix Moeller <mail@felixmoeller.de> + * @author Georg Ehrke <georg@owncloud.com> + * @author Jakob Sack <mail@jakobsack.de> + * @author Jan-Christoph Borchardt <hey@jancborchardt.net> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Jรถrn Friedrich Dreyer <jfd@butonic.de> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Michael Gapczynski <GapczynskiM@gmail.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Olivier Paroz <github@oparoz.com> + * @author Pellaeon Lin <nfsmwlin@gmail.com> + * @author Robin Appelman <icewind@owncloud.com> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author Roeland Jago Douma <rullzer@owncloud.com> + * @author Simon Kรถnnecke <simonkoennecke@gmail.com> + * @author Thomas Mรผller <thomas.mueller@tmit.eu> + * @author Thomas Tanghus <thomas@tanghus.net> + * @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/> + * + */ +use Symfony\Component\Process\ExecutableFinder; + +/** + * Collection of useful functions + */ +class OC_Helper { + private static $templateManager; + + /** + * Creates an absolute url for public use + * @param string $service id + * @param bool $add_slash + * @return string the url + * + * Returns a absolute url to the given service. + */ + public static function linkToPublic($service, $add_slash = false) { + if ($service === 'files') { + $url = OC::$server->getURLGenerator()->getAbsoluteURL('/s'); + } else { + $url = OC::$server->getURLGenerator()->getAbsoluteURL(OC::$server->getURLGenerator()->linkTo('', 'public.php').'?service='.$service); + } + return $url . (($add_slash && $service[strlen($service) - 1] != '/') ? '/' : ''); + } + + /** + * get path to preview of file + * @param string $path path + * @return string the url + * + * Returns the path to the preview of the file. + */ + public static function previewIcon($path) { + return \OC::$server->getURLGenerator()->linkToRoute('core_ajax_preview', ['x' => 32, 'y' => 32, 'file' => $path]); + } + + public static function publicPreviewIcon( $path, $token ) { + return \OC::$server->getURLGenerator()->linkToRoute('core_ajax_public_preview', ['x' => 32, 'y' => 32, 'file' => $path, 't' => $token]); + } + + /** + * Make a human file size + * @param int $bytes file size in bytes + * @return string a human readable file size + * + * Makes 2048 to 2 kB. + */ + public static function humanFileSize($bytes) { + if ($bytes < 0) { + return "?"; + } + if ($bytes < 1024) { + return "$bytes B"; + } + $bytes = round($bytes / 1024, 0); + if ($bytes < 1024) { + return "$bytes KB"; + } + $bytes = round($bytes / 1024, 1); + if ($bytes < 1024) { + return "$bytes MB"; + } + $bytes = round($bytes / 1024, 1); + if ($bytes < 1024) { + return "$bytes GB"; + } + $bytes = round($bytes / 1024, 1); + if ($bytes < 1024) { + return "$bytes TB"; + } + + $bytes = round($bytes / 1024, 1); + return "$bytes PB"; + } + + /** + * Make a php file size + * @param int $bytes file size in bytes + * @return string a php parseable file size + * + * Makes 2048 to 2k and 2^41 to 2048G + */ + public static function phpFileSize($bytes) { + if ($bytes < 0) { + return "?"; + } + if ($bytes < 1024) { + return $bytes . "B"; + } + $bytes = round($bytes / 1024, 1); + if ($bytes < 1024) { + return $bytes . "K"; + } + $bytes = round($bytes / 1024, 1); + if ($bytes < 1024) { + return $bytes . "M"; + } + $bytes = round($bytes / 1024, 1); + return $bytes . "G"; + } + + /** + * Make a computer file size + * @param string $str file size in human readable format + * @return float a file size in bytes + * + * Makes 2kB to 2048. + * + * Inspired by: http://www.php.net/manual/en/function.filesize.php#92418 + */ + public static function computerFileSize($str) { + $str = strtolower($str); + if (is_numeric($str)) { + return floatval($str); + } + + $bytes_array = array( + 'b' => 1, + 'k' => 1024, + 'kb' => 1024, + 'mb' => 1024 * 1024, + 'm' => 1024 * 1024, + 'gb' => 1024 * 1024 * 1024, + 'g' => 1024 * 1024 * 1024, + 'tb' => 1024 * 1024 * 1024 * 1024, + 't' => 1024 * 1024 * 1024 * 1024, + 'pb' => 1024 * 1024 * 1024 * 1024 * 1024, + 'p' => 1024 * 1024 * 1024 * 1024 * 1024, + ); + + $bytes = floatval($str); + + if (preg_match('#([kmgtp]?b?)$#si', $str, $matches) && !empty($bytes_array[$matches[1]])) { + $bytes *= $bytes_array[$matches[1]]; + } else { + return false; + } + + $bytes = round($bytes); + + return $bytes; + } + + /** + * Recursive copying of folders + * @param string $src source folder + * @param string $dest target folder + * + */ + static function copyr($src, $dest) { + if (is_dir($src)) { + if (!is_dir($dest)) { + mkdir($dest); + } + $files = scandir($src); + foreach ($files as $file) { + if ($file != "." && $file != "..") { + self::copyr("$src/$file", "$dest/$file"); + } + } + } elseif (file_exists($src) && !\OC\Files\Filesystem::isFileBlacklisted($src)) { + copy($src, $dest); + } + } + + /** + * Recursive deletion of folders + * @param string $dir path to the folder + * @param bool $deleteSelf if set to false only the content of the folder will be deleted + * @return bool + */ + static function rmdirr($dir, $deleteSelf = true) { + if (is_dir($dir)) { + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($files as $fileInfo) { + /** @var SplFileInfo $fileInfo */ + if ($fileInfo->isDir()) { + rmdir($fileInfo->getRealPath()); + } else { + unlink($fileInfo->getRealPath()); + } + } + if ($deleteSelf) { + rmdir($dir); + } + } elseif (file_exists($dir)) { + if ($deleteSelf) { + unlink($dir); + } + } + if (!$deleteSelf) { + return true; + } + + return !file_exists($dir); + } + + /** + * @return \OC\Files\Type\TemplateManager + */ + static public function getFileTemplateManager() { + if (!self::$templateManager) { + self::$templateManager = new \OC\Files\Type\TemplateManager(); + } + return self::$templateManager; + } + + /** + * detect if a given program is found in the search PATH + * + * @param string $name + * @param bool $path + * @internal param string $program name + * @internal param string $optional search path, defaults to $PATH + * @return bool true if executable program found in path + */ + public static function canExecute($name, $path = false) { + // path defaults to PATH from environment if not set + if ($path === false) { + $path = getenv("PATH"); + } + // check method depends on operating system + if (!strncmp(PHP_OS, "WIN", 3)) { + // on Windows an appropriate COM or EXE file needs to exist + $exts = array(".exe", ".com"); + $check_fn = "file_exists"; + } else { + // anywhere else we look for an executable file of that name + $exts = array(""); + $check_fn = "is_executable"; + } + // Default check will be done with $path directories : + $dirs = explode(PATH_SEPARATOR, $path); + // WARNING : We have to check if open_basedir is enabled : + $obd = OC::$server->getIniWrapper()->getString('open_basedir'); + if ($obd != "none") { + $obd_values = explode(PATH_SEPARATOR, $obd); + if (count($obd_values) > 0 and $obd_values[0]) { + // open_basedir is in effect ! + // We need to check if the program is in one of these dirs : + $dirs = $obd_values; + } + } + foreach ($dirs as $dir) { + foreach ($exts as $ext) { + if ($check_fn("$dir/$name" . $ext)) + return true; + } + } + return false; + } + + /** + * copy the contents of one stream to another + * + * @param resource $source + * @param resource $target + * @return array the number of bytes copied and result + */ + public static function streamCopy($source, $target) { + if (!$source or !$target) { + return array(0, false); + } + $bufSize = 8192; + $result = true; + $count = 0; + while (!feof($source)) { + $buf = fread($source, $bufSize); + $bytesWritten = fwrite($target, $buf); + if ($bytesWritten !== false) { + $count += $bytesWritten; + } + // note: strlen is expensive so only use it when necessary, + // on the last block + if ($bytesWritten === false + || ($bytesWritten < $bufSize && $bytesWritten < strlen($buf)) + ) { + // write error, could be disk full ? + $result = false; + break; + } + } + return array($count, $result); + } + + /** + * Adds a suffix to the name in case the file exists + * + * @param string $path + * @param string $filename + * @return string + */ + public static function buildNotExistingFileName($path, $filename) { + $view = \OC\Files\Filesystem::getView(); + return self::buildNotExistingFileNameForView($path, $filename, $view); + } + + /** + * Adds a suffix to the name in case the file exists + * + * @param string $path + * @param string $filename + * @return string + */ + public static function buildNotExistingFileNameForView($path, $filename, \OC\Files\View $view) { + if ($path === '/') { + $path = ''; + } + if ($pos = strrpos($filename, '.')) { + $name = substr($filename, 0, $pos); + $ext = substr($filename, $pos); + } else { + $name = $filename; + $ext = ''; + } + + $newpath = $path . '/' . $filename; + if ($view->file_exists($newpath)) { + if (preg_match_all('/\((\d+)\)/', $name, $matches, PREG_OFFSET_CAPTURE)) { + //Replace the last "(number)" with "(number+1)" + $last_match = count($matches[0]) - 1; + $counter = $matches[1][$last_match][0] + 1; + $offset = $matches[0][$last_match][1]; + $match_length = strlen($matches[0][$last_match][0]); + } else { + $counter = 2; + $match_length = 0; + $offset = false; + } + do { + if ($offset) { + //Replace the last "(number)" with "(number+1)" + $newname = substr_replace($name, '(' . $counter . ')', $offset, $match_length); + } else { + $newname = $name . ' (' . $counter . ')'; + } + $newpath = $path . '/' . $newname . $ext; + $counter++; + } while ($view->file_exists($newpath)); + } + + return $newpath; + } + + /** + * Checks if $sub is a subdirectory of $parent + * + * @param string $sub + * @param string $parent + * @return bool + */ + public static function isSubDirectory($sub, $parent) { + $realpathSub = realpath($sub); + $realpathParent = realpath($parent); + + // realpath() may return false in case the directory does not exist + // since we can not be sure how different PHP versions may behave here + // we do an additional check whether realpath returned false + if($realpathSub === false || $realpathParent === false) { + return false; + } + + // Check whether $sub is a subdirectory of $parent + if (strpos($realpathSub, $realpathParent) === 0) { + return true; + } + + return false; + } + + /** + * Returns an array with all keys from input lowercased or uppercased. Numbered indices are left as is. + * + * @param array $input The array to work on + * @param int $case Either MB_CASE_UPPER or MB_CASE_LOWER (default) + * @param string $encoding The encoding parameter is the character encoding. Defaults to UTF-8 + * @return array + * + * Returns an array with all keys from input lowercased or uppercased. Numbered indices are left as is. + * based on http://www.php.net/manual/en/function.array-change-key-case.php#107715 + * + */ + public static function mb_array_change_key_case($input, $case = MB_CASE_LOWER, $encoding = 'UTF-8') { + $case = ($case != MB_CASE_UPPER) ? MB_CASE_LOWER : MB_CASE_UPPER; + $ret = array(); + foreach ($input as $k => $v) { + $ret[mb_convert_case($k, $case, $encoding)] = $v; + } + return $ret; + } + + /** + * performs a search in a nested array + * @param array $haystack the array to be searched + * @param string $needle the search string + * @param string $index optional, only search this key name + * @return mixed the key of the matching field, otherwise false + * + * performs a search in a nested array + * + * taken from http://www.php.net/manual/en/function.array-search.php#97645 + */ + public static function recursiveArraySearch($haystack, $needle, $index = null) { + $aIt = new RecursiveArrayIterator($haystack); + $it = new RecursiveIteratorIterator($aIt); + + while ($it->valid()) { + if (((isset($index) AND ($it->key() == $index)) OR (!isset($index))) AND ($it->current() == $needle)) { + return $aIt->key(); + } + + $it->next(); + } + + return false; + } + + /** + * Shortens str to maxlen by replacing characters in the middle with '...', eg. + * ellipsis('a very long string with lots of useless info to make a better example', 14) becomes 'a very ...example' + * + * @param string $str the string + * @param string $maxlen the maximum length of the result + * @return string with at most maxlen characters + */ + public static function ellipsis($str, $maxlen) { + if (strlen($str) > $maxlen) { + $characters = floor($maxlen / 2); + return substr($str, 0, $characters) . '...' . substr($str, -1 * $characters); + } + return $str; + } + + /** + * calculates the maximum upload size respecting system settings, free space and user quota + * + * @param string $dir the current folder where the user currently operates + * @param int $freeSpace the number of bytes free on the storage holding $dir, if not set this will be received from the storage directly + * @return int number of bytes representing + */ + public static function maxUploadFilesize($dir, $freeSpace = null) { + if (is_null($freeSpace) || $freeSpace < 0){ + $freeSpace = self::freeSpace($dir); + } + return min($freeSpace, self::uploadLimit()); + } + + /** + * Calculate free space left within user quota + * + * @param string $dir the current folder where the user currently operates + * @return int number of bytes representing + */ + public static function freeSpace($dir) { + $freeSpace = \OC\Files\Filesystem::free_space($dir); + if ($freeSpace !== \OCP\Files\FileInfo::SPACE_UNKNOWN) { + $freeSpace = max($freeSpace, 0); + return $freeSpace; + } else { + return (INF > 0)? INF: PHP_INT_MAX; // work around https://bugs.php.net/bug.php?id=69188 + } + } + + /** + * Calculate PHP upload limit + * + * @return int PHP upload file size limit + */ + public static function uploadLimit() { + $ini = \OC::$server->getIniWrapper(); + $upload_max_filesize = OCP\Util::computerFileSize($ini->get('upload_max_filesize')); + $post_max_size = OCP\Util::computerFileSize($ini->get('post_max_size')); + if ((int)$upload_max_filesize === 0 and (int)$post_max_size === 0) { + return INF; + } elseif ((int)$upload_max_filesize === 0 or (int)$post_max_size === 0) { + return max($upload_max_filesize, $post_max_size); //only the non 0 value counts + } else { + return min($upload_max_filesize, $post_max_size); + } + } + + /** + * Checks if a function is available + * + * @param string $function_name + * @return bool + */ + public static function is_function_enabled($function_name) { + if (!function_exists($function_name)) { + return false; + } + $ini = \OC::$server->getIniWrapper(); + $disabled = explode(',', $ini->get('disable_functions')); + $disabled = array_map('trim', $disabled); + if (in_array($function_name, $disabled)) { + return false; + } + $disabled = explode(',', $ini->get('suhosin.executor.func.blacklist')); + $disabled = array_map('trim', $disabled); + if (in_array($function_name, $disabled)) { + return false; + } + return true; + } + + /** + * Try to find a program + * Note: currently windows is not supported + * + * @param string $program + * @return null|string + */ + public static function findBinaryPath($program) { + $memcache = \OC::$server->getMemCacheFactory()->create('findBinaryPath'); + if ($memcache->hasKey($program)) { + return $memcache->get($program); + } + $result = null; + if (!\OC_Util::runningOnWindows() && self::is_function_enabled('exec')) { + $exeSniffer = new ExecutableFinder(); + // Returns null if nothing is found + $result = $exeSniffer->find($program); + if (empty($result)) { + $paths = getenv('PATH'); + if (empty($paths)) { + $paths = '/usr/local/bin /usr/bin /opt/bin /bin'; + } else { + $paths = str_replace(':',' ',getenv('PATH')); + } + $command = 'find ' . $paths . ' -name ' . escapeshellarg($program) . ' 2> /dev/null'; + exec($command, $output, $returnCode); + if (count($output) > 0) { + $result = escapeshellcmd($output[0]); + } + } + } + // store the value for 5 minutes + $memcache->set($program, $result, 300); + return $result; + } + + /** + * Calculate the disc space for the given path + * + * @param string $path + * @param \OCP\Files\FileInfo $rootInfo (optional) + * @return array + * @throws \OCP\Files\NotFoundException + */ + public static function getStorageInfo($path, $rootInfo = null) { + // return storage info without adding mount points + $includeExtStorage = \OC::$server->getSystemConfig()->getValue('quota_include_external_storage', false); + + if (!$rootInfo) { + $rootInfo = \OC\Files\Filesystem::getFileInfo($path, false); + } + if (!$rootInfo instanceof \OCP\Files\FileInfo) { + throw new \OCP\Files\NotFoundException(); + } + $used = $rootInfo->getSize(); + if ($used < 0) { + $used = 0; + } + $quota = \OCP\Files\FileInfo::SPACE_UNLIMITED; + $storage = $rootInfo->getStorage(); + $sourceStorage = $storage; + if ($storage->instanceOfStorage('\OC\Files\Storage\Shared')) { + $includeExtStorage = false; + $sourceStorage = $storage->getSourceStorage(); + } + if ($includeExtStorage) { + $quota = OC_Util::getUserQuota(\OCP\User::getUser()); + if ($quota !== \OCP\Files\FileInfo::SPACE_UNLIMITED) { + // always get free space / total space from root + mount points + return self::getGlobalStorageInfo(); + } + } + + // TODO: need a better way to get total space from storage + if ($sourceStorage->instanceOfStorage('\OC\Files\Storage\Wrapper\Quota')) { + /** @var \OC\Files\Storage\Wrapper\Quota $storage */ + $quota = $sourceStorage->getQuota(); + } + $free = $sourceStorage->free_space(''); + if ($free >= 0) { + $total = $free + $used; + } else { + $total = $free; //either unknown or unlimited + } + if ($total > 0) { + if ($quota > 0 && $total > $quota) { + $total = $quota; + } + // prevent division by zero or error codes (negative values) + $relative = round(($used / $total) * 10000) / 100; + } else { + $relative = 0; + } + + $ownerId = $storage->getOwner($path); + $ownerDisplayName = ''; + $owner = \OC::$server->getUserManager()->get($ownerId); + if($owner) { + $ownerDisplayName = $owner->getDisplayName(); + } + + return [ + 'free' => $free, + 'used' => $used, + 'quota' => $quota, + 'total' => $total, + 'relative' => $relative, + 'owner' => $ownerId, + 'ownerDisplayName' => $ownerDisplayName, + ]; + } + + /** + * Get storage info including all mount points and quota + * + * @return array + */ + private static function getGlobalStorageInfo() { + $quota = OC_Util::getUserQuota(\OCP\User::getUser()); + + $rootInfo = \OC\Files\Filesystem::getFileInfo('', 'ext'); + $used = $rootInfo['size']; + if ($used < 0) { + $used = 0; + } + + $total = $quota; + $free = $quota - $used; + + if ($total > 0) { + if ($quota > 0 && $total > $quota) { + $total = $quota; + } + // prevent division by zero or error codes (negative values) + $relative = round(($used / $total) * 10000) / 100; + } else { + $relative = 0; + } + + return array('free' => $free, 'used' => $used, 'total' => $total, 'relative' => $relative); + + } + + /** + * Returns whether the config file is set manually to read-only + * @return bool + */ + public static function isReadOnlyConfigEnabled() { + return \OC::$server->getConfig()->getSystemValue('config_is_read_only', false); + } +} diff --git a/lib/private/legacy/hook.php b/lib/private/legacy/hook.php new file mode 100644 index 00000000000..76f3b657eb9 --- /dev/null +++ b/lib/private/legacy/hook.php @@ -0,0 +1,145 @@ +<?php +/** + * @author Bart Visscher <bartv@thisnet.nl> + * @author Jakob Sack <mail@jakobsack.de> + * @author Jรถrn Friedrich Dreyer <jfd@butonic.de> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author Sam Tuke <mail@samtuke.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/> + * + */ +class OC_Hook{ + public static $thrownExceptions = []; + + static private $registered = array(); + + /** + * connects a function to a hook + * + * @param string $signalClass class name of emitter + * @param string $signalName name of signal + * @param string|object $slotClass class name of slot + * @param string $slotName name of slot + * @return bool + * + * This function makes it very easy to connect to use hooks. + * + * TODO: write example + */ + static public function connect($signalClass, $signalName, $slotClass, $slotName ) { + // If we're trying to connect to an emitting class that isn't + // yet registered, register it + if( !array_key_exists($signalClass, self::$registered )) { + self::$registered[$signalClass] = array(); + } + // If we're trying to connect to an emitting method that isn't + // yet registered, register it with the emitting class + if( !array_key_exists( $signalName, self::$registered[$signalClass] )) { + self::$registered[$signalClass][$signalName] = array(); + } + + // don't connect hooks twice + foreach (self::$registered[$signalClass][$signalName] as $hook) { + if ($hook['class'] === $slotClass and $hook['name'] === $slotName) { + return false; + } + } + // Connect the hook handler to the requested emitter + self::$registered[$signalClass][$signalName][] = array( + "class" => $slotClass, + "name" => $slotName + ); + + // No chance for failure ;-) + return true; + } + + /** + * emits a signal + * + * @param string $signalClass class name of emitter + * @param string $signalName name of signal + * @param mixed $params default: array() array with additional data + * @return bool true if slots exists or false if not + * @throws \OC\HintException + * @throws \OC\ServerNotAvailableException Emits a signal. To get data from the slot use references! + * + * TODO: write example + */ + static public function emit($signalClass, $signalName, $params = []) { + + // Return false if no hook handlers are listening to this + // emitting class + if( !array_key_exists($signalClass, self::$registered )) { + return false; + } + + // Return false if no hook handlers are listening to this + // emitting method + if( !array_key_exists( $signalName, self::$registered[$signalClass] )) { + return false; + } + + // Call all slots + foreach( self::$registered[$signalClass][$signalName] as $i ) { + try { + call_user_func( array( $i["class"], $i["name"] ), $params ); + } catch (Exception $e){ + self::$thrownExceptions[] = $e; + \OC::$server->getLogger()->logException($e); + if($e instanceof \OC\HintException) { + throw $e; + } + if($e instanceof \OC\ServerNotAvailableException) { + throw $e; + } + } + } + + return true; + } + + /** + * clear hooks + * @param string $signalClass + * @param string $signalName + */ + static public function clear($signalClass='', $signalName='') { + if ($signalClass) { + if ($signalName) { + self::$registered[$signalClass][$signalName]=array(); + }else{ + self::$registered[$signalClass]=array(); + } + }else{ + self::$registered=array(); + } + } + + /** + * DO NOT USE! + * For unit tests ONLY! + */ + static public function getHooks() { + return self::$registered; + } +} diff --git a/lib/private/legacy/image.php b/lib/private/legacy/image.php new file mode 100644 index 00000000000..a97347df623 --- /dev/null +++ b/lib/private/legacy/image.php @@ -0,0 +1,1147 @@ +<?php +/** + * @author Andreas Fischer <bantu@owncloud.com> + * @author Bart Visscher <bartv@thisnet.nl> + * @author Bartek Przybylski <bart.p.pl@gmail.com> + * @author Bjรถrn Schieรle <schiessle@owncloud.com> + * @author Byron Marohn <combustible@live.com> + * @author Christopher Schรคpers <kondou@ts.unde.re> + * @author Georg Ehrke <georg@owncloud.com> + * @author j-ed <juergen@eisfair.org> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Johannes Willnecker <johannes@willnecker.com> + * @author Jรถrn Friedrich Dreyer <jfd@butonic.de> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Olivier Paroz <github@oparoz.com> + * @author Robin Appelman <icewind@owncloud.com> + * @author Thomas Mรผller <thomas.mueller@tmit.eu> + * @author Thomas Tanghus <thomas@tanghus.net> + * + * @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/> + * + */ + +/** + * Class for basic image manipulation + */ +class OC_Image implements \OCP\IImage { + /** @var false|resource */ + protected $resource = false; // tmp resource. + /** @var int */ + protected $imageType = IMAGETYPE_PNG; // Default to png if file type isn't evident. + /** @var string */ + protected $mimeType = 'image/png'; // Default to png + /** @var int */ + protected $bitDepth = 24; + /** @var null|string */ + protected $filePath = null; + /** @var finfo */ + private $fileInfo; + /** @var \OCP\ILogger */ + private $logger; + + /** + * Get mime type for an image file. + * + * @param string|null $filePath The path to a local image file. + * @return string The mime type if the it could be determined, otherwise an empty string. + */ + static public function getMimeTypeForFile($filePath) { + // exif_imagetype throws "read error!" if file is less than 12 byte + if ($filePath !== null && filesize($filePath) > 11) { + $imageType = exif_imagetype($filePath); + } else { + $imageType = false; + } + return $imageType ? image_type_to_mime_type($imageType) : ''; + } + + /** + * Constructor. + * + * @param resource|string $imageRef The path to a local file, a base64 encoded string or a resource created by + * an imagecreate* function. + * @param \OCP\ILogger $logger + */ + public function __construct($imageRef = null, $logger = null) { + $this->logger = $logger; + if (is_null($logger)) { + $this->logger = \OC::$server->getLogger(); + } + + if (!extension_loaded('gd') || !function_exists('gd_info')) { + $this->logger->error(__METHOD__ . '(): GD module not installed', array('app' => 'core')); + return false; + } + + if (\OC_Util::fileInfoLoaded()) { + $this->fileInfo = new finfo(FILEINFO_MIME_TYPE); + } + + if (!is_null($imageRef)) { + $this->load($imageRef); + } + } + + /** + * Determine whether the object contains an image resource. + * + * @return bool + */ + public function valid() { // apparently you can't name a method 'empty'... + return is_resource($this->resource); + } + + /** + * Returns the MIME type of the image or an empty string if no image is loaded. + * + * @return string + */ + public function mimeType() { + return $this->valid() ? $this->mimeType : ''; + } + + /** + * Returns the width of the image or -1 if no image is loaded. + * + * @return int + */ + public function width() { + return $this->valid() ? imagesx($this->resource) : -1; + } + + /** + * Returns the height of the image or -1 if no image is loaded. + * + * @return int + */ + public function height() { + return $this->valid() ? imagesy($this->resource) : -1; + } + + /** + * Returns the width when the image orientation is top-left. + * + * @return int + */ + public function widthTopLeft() { + $o = $this->getOrientation(); + $this->logger->debug('OC_Image->widthTopLeft() Orientation: ' . $o, array('app' => 'core')); + switch ($o) { + case -1: + case 1: + case 2: // Not tested + case 3: + case 4: // Not tested + return $this->width(); + case 5: // Not tested + case 6: + case 7: // Not tested + case 8: + return $this->height(); + } + return $this->width(); + } + + /** + * Returns the height when the image orientation is top-left. + * + * @return int + */ + public function heightTopLeft() { + $o = $this->getOrientation(); + $this->logger->debug('OC_Image->heightTopLeft() Orientation: ' . $o, array('app' => 'core')); + switch ($o) { + case -1: + case 1: + case 2: // Not tested + case 3: + case 4: // Not tested + return $this->height(); + case 5: // Not tested + case 6: + case 7: // Not tested + case 8: + return $this->width(); + } + return $this->height(); + } + + /** + * Outputs the image. + * + * @param string $mimeType + * @return bool + */ + public function show($mimeType = null) { + if ($mimeType === null) { + $mimeType = $this->mimeType(); + } + header('Content-Type: ' . $mimeType); + return $this->_output(null, $mimeType); + } + + /** + * Saves the image. + * + * @param string $filePath + * @param string $mimeType + * @return bool + */ + + public function save($filePath = null, $mimeType = null) { + if ($mimeType === null) { + $mimeType = $this->mimeType(); + } + if ($filePath === null && $this->filePath === null) { + $this->logger->error(__METHOD__ . '(): called with no path.', array('app' => 'core')); + return false; + } elseif ($filePath === null && $this->filePath !== null) { + $filePath = $this->filePath; + } + return $this->_output($filePath, $mimeType); + } + + /** + * Outputs/saves the image. + * + * @param string $filePath + * @param string $mimeType + * @return bool + * @throws Exception + */ + private function _output($filePath = null, $mimeType = null) { + if ($filePath) { + if (!file_exists(dirname($filePath))) + mkdir(dirname($filePath), 0777, true); + if (!is_writable(dirname($filePath))) { + $this->logger->error(__METHOD__ . '(): Directory \'' . dirname($filePath) . '\' is not writable.', array('app' => 'core')); + return false; + } elseif (is_writable(dirname($filePath)) && file_exists($filePath) && !is_writable($filePath)) { + $this->logger->error(__METHOD__ . '(): File \'' . $filePath . '\' is not writable.', array('app' => 'core')); + return false; + } + } + if (!$this->valid()) { + return false; + } + + $imageType = $this->imageType; + if ($mimeType !== null) { + switch ($mimeType) { + case 'image/gif': + $imageType = IMAGETYPE_GIF; + break; + case 'image/jpeg': + $imageType = IMAGETYPE_JPEG; + break; + case 'image/png': + $imageType = IMAGETYPE_PNG; + break; + case 'image/x-xbitmap': + $imageType = IMAGETYPE_XBM; + break; + case 'image/bmp': + case 'image/x-ms-bmp': + $imageType = IMAGETYPE_BMP; + break; + default: + throw new Exception('\OC_Image::_output(): "' . $mimeType . '" is not supported when forcing a specific output format'); + } + } + + switch ($imageType) { + case IMAGETYPE_GIF: + $retVal = imagegif($this->resource, $filePath); + break; + case IMAGETYPE_JPEG: + $retVal = imagejpeg($this->resource, $filePath); + break; + case IMAGETYPE_PNG: + $retVal = imagepng($this->resource, $filePath); + break; + case IMAGETYPE_XBM: + if (function_exists('imagexbm')) { + $retVal = imagexbm($this->resource, $filePath); + } else { + throw new Exception('\OC_Image::_output(): imagexbm() is not supported.'); + } + + break; + case IMAGETYPE_WBMP: + $retVal = imagewbmp($this->resource, $filePath); + break; + case IMAGETYPE_BMP: + $retVal = imagebmp($this->resource, $filePath, $this->bitDepth); + break; + default: + $retVal = imagepng($this->resource, $filePath); + } + return $retVal; + } + + /** + * Prints the image when called as $image(). + */ + public function __invoke() { + return $this->show(); + } + + /** + * @return resource Returns the image resource in any. + */ + public function resource() { + return $this->resource; + } + + /** + * @return null|string Returns the raw image data. + */ + public function data() { + if (!$this->valid()) { + return null; + } + ob_start(); + switch ($this->mimeType) { + case "image/png": + $res = imagepng($this->resource); + break; + case "image/jpeg": + $res = imagejpeg($this->resource); + break; + case "image/gif": + $res = imagegif($this->resource); + break; + default: + $res = imagepng($this->resource); + $this->logger->info('OC_Image->data. Could not guess mime-type, defaulting to png', array('app' => 'core')); + break; + } + if (!$res) { + $this->logger->error('OC_Image->data. Error getting image data.', array('app' => 'core')); + } + return ob_get_clean(); + } + + /** + * @return string - base64 encoded, which is suitable for embedding in a VCard. + */ + function __toString() { + return base64_encode($this->data()); + } + + /** + * (I'm open for suggestions on better method name ;) + * Get the orientation based on EXIF data. + * + * @return int The orientation or -1 if no EXIF data is available. + */ + public function getOrientation() { + if ($this->imageType !== IMAGETYPE_JPEG) { + $this->logger->debug('OC_Image->fixOrientation() Image is not a JPEG.', array('app' => 'core')); + return -1; + } + if (!is_callable('exif_read_data')) { + $this->logger->debug('OC_Image->fixOrientation() Exif module not enabled.', array('app' => 'core')); + return -1; + } + if (!$this->valid()) { + $this->logger->debug('OC_Image->fixOrientation() No image loaded.', array('app' => 'core')); + return -1; + } + if (is_null($this->filePath) || !is_readable($this->filePath)) { + $this->logger->debug('OC_Image->fixOrientation() No readable file path set.', array('app' => 'core')); + return -1; + } + $exif = @exif_read_data($this->filePath, 'IFD0'); + if (!$exif) { + return -1; + } + if (!isset($exif['Orientation'])) { + return -1; + } + return $exif['Orientation']; + } + + /** + * (I'm open for suggestions on better method name ;) + * Fixes orientation based on EXIF data. + * + * @return bool. + */ + public function fixOrientation() { + $o = $this->getOrientation(); + $this->logger->debug('OC_Image->fixOrientation() Orientation: ' . $o, array('app' => 'core')); + $rotate = 0; + $flip = false; + switch ($o) { + case -1: + return false; //Nothing to fix + case 1: + $rotate = 0; + break; + case 2: + $rotate = 0; + $flip = true; + break; + case 3: + $rotate = 180; + break; + case 4: + $rotate = 180; + $flip = true; + break; + case 5: + $rotate = 90; + $flip = true; + break; + case 6: + $rotate = 270; + break; + case 7: + $rotate = 270; + $flip = true; + break; + case 8: + $rotate = 90; + break; + } + if($flip && function_exists('imageflip')) { + imageflip($this->resource, IMG_FLIP_HORIZONTAL); + } + if ($rotate) { + $res = imagerotate($this->resource, $rotate, 0); + if ($res) { + if (imagealphablending($res, true)) { + if (imagesavealpha($res, true)) { + imagedestroy($this->resource); + $this->resource = $res; + return true; + } else { + $this->logger->debug('OC_Image->fixOrientation() Error during alpha-saving', array('app' => 'core')); + return false; + } + } else { + $this->logger->debug('OC_Image->fixOrientation() Error during alpha-blending', array('app' => 'core')); + return false; + } + } else { + $this->logger->debug('OC_Image->fixOrientation() Error during orientation fixing', array('app' => 'core')); + return false; + } + } + return false; + } + + /** + * Loads an image from a local file, a base64 encoded string or a resource created by an imagecreate* function. + * + * @param resource|string $imageRef The path to a local file, a base64 encoded string or a resource created by an imagecreate* function or a file resource (file handle ). + * @return resource|false An image resource or false on error + */ + public function load($imageRef) { + if (is_resource($imageRef)) { + if (get_resource_type($imageRef) == 'gd') { + $this->resource = $imageRef; + return $this->resource; + } elseif (in_array(get_resource_type($imageRef), array('file', 'stream'))) { + return $this->loadFromFileHandle($imageRef); + } + } elseif ($this->loadFromBase64($imageRef) !== false) { + return $this->resource; + } elseif ($this->loadFromFile($imageRef) !== false) { + return $this->resource; + } elseif ($this->loadFromData($imageRef) !== false) { + return $this->resource; + } + $this->logger->debug(__METHOD__ . '(): could not load anything. Giving up!', array('app' => 'core')); + return false; + } + + /** + * Loads an image from an open file handle. + * It is the responsibility of the caller to position the pointer at the correct place and to close the handle again. + * + * @param resource $handle + * @return resource|false An image resource or false on error + */ + public function loadFromFileHandle($handle) { + $contents = stream_get_contents($handle); + if ($this->loadFromData($contents)) { + return $this->resource; + } + return false; + } + + /** + * Loads an image from a local file. + * + * @param bool|string $imagePath The path to a local file. + * @return bool|resource An image resource or false on error + */ + public function loadFromFile($imagePath = false) { + // exif_imagetype throws "read error!" if file is less than 12 byte + if (!@is_file($imagePath) || !file_exists($imagePath) || filesize($imagePath) < 12 || !is_readable($imagePath)) { + return false; + } + $iType = exif_imagetype($imagePath); + switch ($iType) { + case IMAGETYPE_GIF: + if (imagetypes() & IMG_GIF) { + $this->resource = imagecreatefromgif($imagePath); + // Preserve transparency + imagealphablending($this->resource, true); + imagesavealpha($this->resource, true); + } else { + $this->logger->debug('OC_Image->loadFromFile, GIF images not supported: ' . $imagePath, array('app' => 'core')); + } + break; + case IMAGETYPE_JPEG: + if (imagetypes() & IMG_JPG) { + $this->resource = imagecreatefromjpeg($imagePath); + } else { + $this->logger->debug('OC_Image->loadFromFile, JPG images not supported: ' . $imagePath, array('app' => 'core')); + } + break; + case IMAGETYPE_PNG: + if (imagetypes() & IMG_PNG) { + $this->resource = imagecreatefrompng($imagePath); + // Preserve transparency + imagealphablending($this->resource, true); + imagesavealpha($this->resource, true); + } else { + $this->logger->debug('OC_Image->loadFromFile, PNG images not supported: ' . $imagePath, array('app' => 'core')); + } + break; + case IMAGETYPE_XBM: + if (imagetypes() & IMG_XPM) { + $this->resource = imagecreatefromxbm($imagePath); + } else { + $this->logger->debug('OC_Image->loadFromFile, XBM/XPM images not supported: ' . $imagePath, array('app' => 'core')); + } + break; + case IMAGETYPE_WBMP: + if (imagetypes() & IMG_WBMP) { + $this->resource = imagecreatefromwbmp($imagePath); + } else { + $this->logger->debug('OC_Image->loadFromFile, WBMP images not supported: ' . $imagePath, array('app' => 'core')); + } + break; + case IMAGETYPE_BMP: + $this->resource = $this->imagecreatefrombmp($imagePath); + break; + /* + case IMAGETYPE_TIFF_II: // (intel byte order) + break; + case IMAGETYPE_TIFF_MM: // (motorola byte order) + break; + case IMAGETYPE_JPC: + break; + case IMAGETYPE_JP2: + break; + case IMAGETYPE_JPX: + break; + case IMAGETYPE_JB2: + break; + case IMAGETYPE_SWC: + break; + case IMAGETYPE_IFF: + break; + case IMAGETYPE_ICO: + break; + case IMAGETYPE_SWF: + break; + case IMAGETYPE_PSD: + break; + */ + default: + + // this is mostly file created from encrypted file + $this->resource = imagecreatefromstring(\OC\Files\Filesystem::file_get_contents(\OC\Files\Filesystem::getLocalPath($imagePath))); + $iType = IMAGETYPE_PNG; + $this->logger->debug('OC_Image->loadFromFile, Default', array('app' => 'core')); + break; + } + if ($this->valid()) { + $this->imageType = $iType; + $this->mimeType = image_type_to_mime_type($iType); + $this->filePath = $imagePath; + } + return $this->resource; + } + + /** + * Loads an image from a string of data. + * + * @param string $str A string of image data as read from a file. + * @return bool|resource An image resource or false on error + */ + public function loadFromData($str) { + if (is_resource($str)) { + return false; + } + $this->resource = @imagecreatefromstring($str); + if ($this->fileInfo) { + $this->mimeType = $this->fileInfo->buffer($str); + } + if (is_resource($this->resource)) { + imagealphablending($this->resource, false); + imagesavealpha($this->resource, true); + } + + if (!$this->resource) { + $this->logger->debug('OC_Image->loadFromFile, could not load', array('app' => 'core')); + return false; + } + return $this->resource; + } + + /** + * Loads an image from a base64 encoded string. + * + * @param string $str A string base64 encoded string of image data. + * @return bool|resource An image resource or false on error + */ + public function loadFromBase64($str) { + if (!is_string($str)) { + return false; + } + $data = base64_decode($str); + if ($data) { // try to load from string data + $this->resource = @imagecreatefromstring($data); + if ($this->fileInfo) { + $this->mimeType = $this->fileInfo->buffer($data); + } + if (!$this->resource) { + $this->logger->debug('OC_Image->loadFromBase64, could not load', array('app' => 'core')); + return false; + } + return $this->resource; + } else { + return false; + } + } + + /** + * Create a new image from file or URL + * + * @link http://www.programmierer-forum.de/function-imagecreatefrombmp-laeuft-mit-allen-bitraten-t143137.htm + * @version 1.00 + * @param string $fileName <p> + * Path to the BMP image. + * </p> + * @return bool|resource an image resource identifier on success, <b>FALSE</b> on errors. + */ + private function imagecreatefrombmp($fileName) { + if (!($fh = fopen($fileName, 'rb'))) { + $this->logger->warning('imagecreatefrombmp: Can not open ' . $fileName, array('app' => 'core')); + return false; + } + // read file header + $meta = unpack('vtype/Vfilesize/Vreserved/Voffset', fread($fh, 14)); + // check for bitmap + if ($meta['type'] != 19778) { + fclose($fh); + $this->logger->warning('imagecreatefrombmp: Can not open ' . $fileName . ' is not a bitmap!', array('app' => 'core')); + return false; + } + // read image header + $meta += unpack('Vheadersize/Vwidth/Vheight/vplanes/vbits/Vcompression/Vimagesize/Vxres/Vyres/Vcolors/Vimportant', fread($fh, 40)); + // read additional 16bit header + if ($meta['bits'] == 16) { + $meta += unpack('VrMask/VgMask/VbMask', fread($fh, 12)); + } + // set bytes and padding + $meta['bytes'] = $meta['bits'] / 8; + $this->bitDepth = $meta['bits']; //remember the bit depth for the imagebmp call + $meta['decal'] = 4 - (4 * (($meta['width'] * $meta['bytes'] / 4) - floor($meta['width'] * $meta['bytes'] / 4))); + if ($meta['decal'] == 4) { + $meta['decal'] = 0; + } + // obtain imagesize + if ($meta['imagesize'] < 1) { + $meta['imagesize'] = $meta['filesize'] - $meta['offset']; + // in rare cases filesize is equal to offset so we need to read physical size + if ($meta['imagesize'] < 1) { + $meta['imagesize'] = @filesize($fileName) - $meta['offset']; + if ($meta['imagesize'] < 1) { + fclose($fh); + $this->logger->warning('imagecreatefrombmp: Can not obtain file size of ' . $fileName . ' is not a bitmap!', array('app' => 'core')); + return false; + } + } + } + // calculate colors + $meta['colors'] = !$meta['colors'] ? pow(2, $meta['bits']) : $meta['colors']; + // read color palette + $palette = array(); + if ($meta['bits'] < 16) { + $palette = unpack('l' . $meta['colors'], fread($fh, $meta['colors'] * 4)); + // in rare cases the color value is signed + if ($palette[1] < 0) { + foreach ($palette as $i => $color) { + $palette[$i] = $color + 16777216; + } + } + } + // create gd image + $im = imagecreatetruecolor($meta['width'], $meta['height']); + if ($im == false) { + fclose($fh); + $this->logger->warning( + 'imagecreatefrombmp: imagecreatetruecolor failed for file "' . $fileName . '" with dimensions ' . $meta['width'] . 'x' . $meta['height'], + array('app' => 'core')); + return false; + } + + $data = fread($fh, $meta['imagesize']); + $p = 0; + $vide = chr(0); + $y = $meta['height'] - 1; + $error = 'imagecreatefrombmp: ' . $fileName . ' has not enough data!'; + // loop through the image data beginning with the lower left corner + while ($y >= 0) { + $x = 0; + while ($x < $meta['width']) { + switch ($meta['bits']) { + case 32: + case 24: + if (!($part = substr($data, $p, 3))) { + $this->logger->warning($error, array('app' => 'core')); + return $im; + } + $color = unpack('V', $part . $vide); + break; + case 16: + if (!($part = substr($data, $p, 2))) { + fclose($fh); + $this->logger->warning($error, array('app' => 'core')); + return $im; + } + $color = unpack('v', $part); + $color[1] = (($color[1] & 0xf800) >> 8) * 65536 + (($color[1] & 0x07e0) >> 3) * 256 + (($color[1] & 0x001f) << 3); + break; + case 8: + $color = unpack('n', $vide . substr($data, $p, 1)); + $color[1] = $palette[$color[1] + 1]; + break; + case 4: + $color = unpack('n', $vide . substr($data, floor($p), 1)); + $color[1] = ($p * 2) % 2 == 0 ? $color[1] >> 4 : $color[1] & 0x0F; + $color[1] = $palette[$color[1] + 1]; + break; + case 1: + $color = unpack('n', $vide . substr($data, floor($p), 1)); + switch (($p * 8) % 8) { + case 0: + $color[1] = $color[1] >> 7; + break; + case 1: + $color[1] = ($color[1] & 0x40) >> 6; + break; + case 2: + $color[1] = ($color[1] & 0x20) >> 5; + break; + case 3: + $color[1] = ($color[1] & 0x10) >> 4; + break; + case 4: + $color[1] = ($color[1] & 0x8) >> 3; + break; + case 5: + $color[1] = ($color[1] & 0x4) >> 2; + break; + case 6: + $color[1] = ($color[1] & 0x2) >> 1; + break; + case 7: + $color[1] = ($color[1] & 0x1); + break; + } + $color[1] = $palette[$color[1] + 1]; + break; + default: + fclose($fh); + $this->logger->warning('imagecreatefrombmp: ' . $fileName . ' has ' . $meta['bits'] . ' bits and this is not supported!', array('app' => 'core')); + return false; + } + imagesetpixel($im, $x, $y, $color[1]); + $x++; + $p += $meta['bytes']; + } + $y--; + $p += $meta['decal']; + } + fclose($fh); + return $im; + } + + /** + * Resizes the image preserving ratio. + * + * @param integer $maxSize The maximum size of either the width or height. + * @return bool + */ + public function resize($maxSize) { + if (!$this->valid()) { + $this->logger->error(__METHOD__ . '(): No image loaded', array('app' => 'core')); + return false; + } + $widthOrig = imageSX($this->resource); + $heightOrig = imageSY($this->resource); + $ratioOrig = $widthOrig / $heightOrig; + + if ($ratioOrig > 1) { + $newHeight = round($maxSize / $ratioOrig); + $newWidth = $maxSize; + } else { + $newWidth = round($maxSize * $ratioOrig); + $newHeight = $maxSize; + } + + $this->preciseResize(round($newWidth), round($newHeight)); + return true; + } + + /** + * @param int $width + * @param int $height + * @return bool + */ + public function preciseResize($width, $height) { + if (!$this->valid()) { + $this->logger->error(__METHOD__ . '(): No image loaded', array('app' => 'core')); + return false; + } + $widthOrig = imageSX($this->resource); + $heightOrig = imageSY($this->resource); + $process = imagecreatetruecolor($width, $height); + + if ($process == false) { + $this->logger->error(__METHOD__ . '(): Error creating true color image', array('app' => 'core')); + imagedestroy($process); + return false; + } + + // preserve transparency + if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) { + imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127)); + imagealphablending($process, false); + imagesavealpha($process, true); + } + + imagecopyresampled($process, $this->resource, 0, 0, 0, 0, $width, $height, $widthOrig, $heightOrig); + if ($process == false) { + $this->logger->error(__METHOD__ . '(): Error re-sampling process image', array('app' => 'core')); + imagedestroy($process); + return false; + } + imagedestroy($this->resource); + $this->resource = $process; + return true; + } + + /** + * Crops the image to the middle square. If the image is already square it just returns. + * + * @param int $size maximum size for the result (optional) + * @return bool for success or failure + */ + public function centerCrop($size = 0) { + if (!$this->valid()) { + $this->logger->error('OC_Image->centerCrop, No image loaded', array('app' => 'core')); + return false; + } + $widthOrig = imageSX($this->resource); + $heightOrig = imageSY($this->resource); + if ($widthOrig === $heightOrig and $size == 0) { + return true; + } + $ratioOrig = $widthOrig / $heightOrig; + $width = $height = min($widthOrig, $heightOrig); + + if ($ratioOrig > 1) { + $x = ($widthOrig / 2) - ($width / 2); + $y = 0; + } else { + $y = ($heightOrig / 2) - ($height / 2); + $x = 0; + } + if ($size > 0) { + $targetWidth = $size; + $targetHeight = $size; + } else { + $targetWidth = $width; + $targetHeight = $height; + } + $process = imagecreatetruecolor($targetWidth, $targetHeight); + if ($process == false) { + $this->logger->error('OC_Image->centerCrop, Error creating true color image', array('app' => 'core')); + imagedestroy($process); + return false; + } + + // preserve transparency + if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) { + imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127)); + imagealphablending($process, false); + imagesavealpha($process, true); + } + + imagecopyresampled($process, $this->resource, 0, 0, $x, $y, $targetWidth, $targetHeight, $width, $height); + if ($process == false) { + $this->logger->error('OC_Image->centerCrop, Error re-sampling process image ' . $width . 'x' . $height, array('app' => 'core')); + imagedestroy($process); + return false; + } + imagedestroy($this->resource); + $this->resource = $process; + return true; + } + + /** + * Crops the image from point $x$y with dimension $wx$h. + * + * @param int $x Horizontal position + * @param int $y Vertical position + * @param int $w Width + * @param int $h Height + * @return bool for success or failure + */ + public function crop($x, $y, $w, $h) { + if (!$this->valid()) { + $this->logger->error(__METHOD__ . '(): No image loaded', array('app' => 'core')); + return false; + } + $process = imagecreatetruecolor($w, $h); + if ($process == false) { + $this->logger->error(__METHOD__ . '(): Error creating true color image', array('app' => 'core')); + imagedestroy($process); + return false; + } + + // preserve transparency + if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) { + imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127)); + imagealphablending($process, false); + imagesavealpha($process, true); + } + + imagecopyresampled($process, $this->resource, 0, 0, $x, $y, $w, $h, $w, $h); + if ($process == false) { + $this->logger->error(__METHOD__ . '(): Error re-sampling process image ' . $w . 'x' . $h, array('app' => 'core')); + imagedestroy($process); + return false; + } + imagedestroy($this->resource); + $this->resource = $process; + return true; + } + + /** + * Resizes the image to fit within a boundary while preserving ratio. + * + * Warning: Images smaller than $maxWidth x $maxHeight will end up being scaled up + * + * @param integer $maxWidth + * @param integer $maxHeight + * @return bool + */ + public function fitIn($maxWidth, $maxHeight) { + if (!$this->valid()) { + $this->logger->error(__METHOD__ . '(): No image loaded', array('app' => 'core')); + return false; + } + $widthOrig = imageSX($this->resource); + $heightOrig = imageSY($this->resource); + $ratio = $widthOrig / $heightOrig; + + $newWidth = min($maxWidth, $ratio * $maxHeight); + $newHeight = min($maxHeight, $maxWidth / $ratio); + + $this->preciseResize(round($newWidth), round($newHeight)); + return true; + } + + /** + * Shrinks larger images to fit within specified boundaries while preserving ratio. + * + * @param integer $maxWidth + * @param integer $maxHeight + * @return bool + */ + public function scaleDownToFit($maxWidth, $maxHeight) { + if (!$this->valid()) { + $this->logger->error(__METHOD__ . '(): No image loaded', array('app' => 'core')); + return false; + } + $widthOrig = imageSX($this->resource); + $heightOrig = imageSY($this->resource); + + if ($widthOrig > $maxWidth || $heightOrig > $maxHeight) { + return $this->fitIn($maxWidth, $maxHeight); + } + + return false; + } + + /** + * Destroys the current image and resets the object + */ + public function destroy() { + if ($this->valid()) { + imagedestroy($this->resource); + } + $this->resource = null; + } + + public function __destruct() { + $this->destroy(); + } +} + +if (!function_exists('imagebmp')) { + /** + * Output a BMP image to either the browser or a file + * + * @link http://www.ugia.cn/wp-data/imagebmp.php + * @author legend <legendsky@hotmail.com> + * @link http://www.programmierer-forum.de/imagebmp-gute-funktion-gefunden-t143716.htm + * @author mgutt <marc@gutt.it> + * @version 1.00 + * @param string $fileName [optional] <p>The path to save the file to.</p> + * @param int $bit [optional] <p>Bit depth, (default is 24).</p> + * @param int $compression [optional] + * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure. + */ + function imagebmp($im, $fileName = '', $bit = 24, $compression = 0) { + if (!in_array($bit, array(1, 4, 8, 16, 24, 32))) { + $bit = 24; + } else if ($bit == 32) { + $bit = 24; + } + $bits = pow(2, $bit); + imagetruecolortopalette($im, true, $bits); + $width = imagesx($im); + $height = imagesy($im); + $colorsNum = imagecolorstotal($im); + $rgbQuad = ''; + if ($bit <= 8) { + for ($i = 0; $i < $colorsNum; $i++) { + $colors = imagecolorsforindex($im, $i); + $rgbQuad .= chr($colors['blue']) . chr($colors['green']) . chr($colors['red']) . "\0"; + } + $bmpData = ''; + if ($compression == 0 || $bit < 8) { + $compression = 0; + $extra = ''; + $padding = 4 - ceil($width / (8 / $bit)) % 4; + if ($padding % 4 != 0) { + $extra = str_repeat("\0", $padding); + } + for ($j = $height - 1; $j >= 0; $j--) { + $i = 0; + while ($i < $width) { + $bin = 0; + $limit = $width - $i < 8 / $bit ? (8 / $bit - $width + $i) * $bit : 0; + for ($k = 8 - $bit; $k >= $limit; $k -= $bit) { + $index = imagecolorat($im, $i, $j); + $bin |= $index << $k; + $i++; + } + $bmpData .= chr($bin); + } + $bmpData .= $extra; + } + } // RLE8 + else if ($compression == 1 && $bit == 8) { + for ($j = $height - 1; $j >= 0; $j--) { + $lastIndex = "\0"; + $sameNum = 0; + for ($i = 0; $i <= $width; $i++) { + $index = imagecolorat($im, $i, $j); + if ($index !== $lastIndex || $sameNum > 255) { + if ($sameNum != 0) { + $bmpData .= chr($sameNum) . chr($lastIndex); + } + $lastIndex = $index; + $sameNum = 1; + } else { + $sameNum++; + } + } + $bmpData .= "\0\0"; + } + $bmpData .= "\0\1"; + } + $sizeQuad = strlen($rgbQuad); + $sizeData = strlen($bmpData); + } else { + $extra = ''; + $padding = 4 - ($width * ($bit / 8)) % 4; + if ($padding % 4 != 0) { + $extra = str_repeat("\0", $padding); + } + $bmpData = ''; + for ($j = $height - 1; $j >= 0; $j--) { + for ($i = 0; $i < $width; $i++) { + $index = imagecolorat($im, $i, $j); + $colors = imagecolorsforindex($im, $index); + if ($bit == 16) { + $bin = 0 << $bit; + $bin |= ($colors['red'] >> 3) << 10; + $bin |= ($colors['green'] >> 3) << 5; + $bin |= $colors['blue'] >> 3; + $bmpData .= pack("v", $bin); + } else { + $bmpData .= pack("c*", $colors['blue'], $colors['green'], $colors['red']); + } + } + $bmpData .= $extra; + } + $sizeQuad = 0; + $sizeData = strlen($bmpData); + $colorsNum = 0; + } + $fileHeader = 'BM' . pack('V3', 54 + $sizeQuad + $sizeData, 0, 54 + $sizeQuad); + $infoHeader = pack('V3v2V*', 0x28, $width, $height, 1, $bit, $compression, $sizeData, 0, 0, $colorsNum, 0); + if ($fileName != '') { + $fp = fopen($fileName, 'wb'); + fwrite($fp, $fileHeader . $infoHeader . $rgbQuad . $bmpData); + fclose($fp); + return true; + } + echo $fileHeader . $infoHeader . $rgbQuad . $bmpData; + return true; + } +} + +if (!function_exists('exif_imagetype')) { + /** + * Workaround if exif_imagetype does not exist + * + * @link http://www.php.net/manual/en/function.exif-imagetype.php#80383 + * @param string $fileName + * @return string|boolean + */ + function exif_imagetype($fileName) { + if (($info = getimagesize($fileName)) !== false) { + return $info[2]; + } + return false; + } +} diff --git a/lib/private/legacy/installer.php b/lib/private/legacy/installer.php new file mode 100644 index 00000000000..24c79b2dd8c --- /dev/null +++ b/lib/private/legacy/installer.php @@ -0,0 +1,638 @@ +<?php +/** + * @author Arthur Schiwon <blizzz@owncloud.com> + * @author Bart Visscher <bartv@thisnet.nl> + * @author Brice Maron <brice@bmaron.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Christopher Schรคpers <kondou@ts.unde.re> + * @author Frank Karlitschek <frank@owncloud.org> + * @author Georg Ehrke <georg@owncloud.com> + * @author Jakob Sack <mail@jakobsack.de> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Jรถrn Friedrich Dreyer <jfd@butonic.de> + * @author Kamil Domanski <kdomanski@kdemail.net> + * @author Lukas Reschke <lukas@owncloud.com> + * @author michag86 <micha_g@arcor.de> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author root <root@oc.(none)> + * @author Thomas Mรผller <thomas.mueller@tmit.eu> + * @author Thomas Tanghus <thomas@tanghus.net> + * + * @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/> + * + */ + +use OC\App\CodeChecker\CodeChecker; +use OC\App\CodeChecker\EmptyCheck; +use OC\App\CodeChecker\PrivateCheck; +use OC\OCSClient; + +/** + * This class provides the functionality needed to install, update and remove plugins/apps + */ +class OC_Installer{ + + /** + * + * This function installs an app. All information needed are passed in the + * associative array $data. + * The following keys are required: + * - source: string, can be "path" or "http" + * + * One of the following keys is required: + * - path: path to the file containing the app + * - href: link to the downloadable file containing the app + * + * The following keys are optional: + * - pretend: boolean, if set true the system won't do anything + * - noinstall: boolean, if true appinfo/install.php won't be loaded + * - inactive: boolean, if set true the appconfig/app.sample.php won't be + * renamed + * + * This function works as follows + * -# fetching the file + * -# unzipping it + * -# check the code + * -# installing the database at appinfo/database.xml + * -# including appinfo/install.php + * -# setting the installed version + * + * It is the task of oc_app_install to create the tables and do whatever is + * needed to get the app working. + * + * Installs an app + * @param array $data with all information + * @throws \Exception + * @return integer + */ + public static function installApp( $data = array()) { + $l = \OC::$server->getL10N('lib'); + + list($extractDir, $path) = self::downloadApp($data); + + $info = self::checkAppsIntegrity($data, $extractDir, $path); + $appId = OC_App::cleanAppId($info['id']); + $basedir = OC_App::getInstallPath().'/'.$appId; + //check if the destination directory already exists + if(is_dir($basedir)) { + OC_Helper::rmdirr($extractDir); + if($data['source']=='http') { + unlink($path); + } + throw new \Exception($l->t("App directory already exists")); + } + + if(!empty($data['pretent'])) { + return false; + } + + //copy the app to the correct place + if(@!mkdir($basedir)) { + OC_Helper::rmdirr($extractDir); + if($data['source']=='http') { + unlink($path); + } + throw new \Exception($l->t("Can't create app folder. Please fix permissions. %s", array($basedir))); + } + + $extractDir .= '/' . $info['id']; + if(!file_exists($extractDir)) { + OC_Helper::rmdirr($basedir); + throw new \Exception($l->t("Archive does not contain a directory named %s", $info['id'])); + } + OC_Helper::copyr($extractDir, $basedir); + + //remove temporary files + OC_Helper::rmdirr($extractDir); + + //install the database + if(is_file($basedir.'/appinfo/database.xml')) { + if (\OC::$server->getAppConfig()->getValue($info['id'], 'installed_version') === null) { + OC_DB::createDbFromStructure($basedir.'/appinfo/database.xml'); + } else { + OC_DB::updateDbFromStructure($basedir.'/appinfo/database.xml'); + } + } + + //run appinfo/install.php + if((!isset($data['noinstall']) or $data['noinstall']==false)) { + self::includeAppScript($basedir . '/appinfo/install.php'); + } + + //set the installed version + \OC::$server->getAppConfig()->setValue($info['id'], 'installed_version', OC_App::getAppVersion($info['id'])); + \OC::$server->getAppConfig()->setValue($info['id'], 'enabled', 'no'); + + //set remote/public handelers + foreach($info['remote'] as $name=>$path) { + OCP\CONFIG::setAppValue('core', 'remote_'.$name, $info['id'].'/'.$path); + } + foreach($info['public'] as $name=>$path) { + OCP\CONFIG::setAppValue('core', 'public_'.$name, $info['id'].'/'.$path); + } + + OC_App::setAppTypes($info['id']); + + return $info['id']; + } + + /** + * @brief checks whether or not an app is installed + * @param string $app app + * @returns bool + * + * Checks whether or not an app is installed, i.e. registered in apps table. + */ + public static function isInstalled( $app ) { + return (\OC::$server->getAppConfig()->getValue($app, "installed_version") !== null); + } + + /** + * @brief Update an application + * @param array $info + * @param bool $isShipped + * @throws Exception + * @return bool + * + * This function could work like described below, but currently it disables and then + * enables the app again. This does result in an updated app. + * + * + * This function installs an app. All information needed are passed in the + * associative array $info. + * The following keys are required: + * - source: string, can be "path" or "http" + * + * One of the following keys is required: + * - path: path to the file containing the app + * - href: link to the downloadable file containing the app + * + * The following keys are optional: + * - pretend: boolean, if set true the system won't do anything + * - noupgrade: boolean, if true appinfo/upgrade.php won't be loaded + * + * This function works as follows + * -# fetching the file + * -# removing the old files + * -# unzipping new file + * -# including appinfo/upgrade.php + * -# setting the installed version + * + * upgrade.php can determine the current installed version of the app using + * "\OC::$server->getAppConfig()->getValue($appid, 'installed_version')" + */ + public static function updateApp($info=array(), $isShipped=false) { + list($extractDir, $path) = self::downloadApp($info); + $info = self::checkAppsIntegrity($info, $extractDir, $path, $isShipped); + + $currentDir = OC_App::getAppPath($info['id']); + $basedir = OC_App::getInstallPath(); + $basedir .= '/'; + $basedir .= $info['id']; + + if($currentDir !== false && is_writable($currentDir)) { + $basedir = $currentDir; + } + if(is_dir($basedir)) { + OC_Helper::rmdirr($basedir); + } + + $appInExtractDir = $extractDir; + if (substr($extractDir, -1) !== '/') { + $appInExtractDir .= '/'; + } + + $appInExtractDir .= $info['id']; + OC_Helper::copyr($appInExtractDir, $basedir); + OC_Helper::rmdirr($extractDir); + + return OC_App::updateApp($info['id']); + } + + /** + * update an app by it's id + * + * @param integer $ocsId + * @return bool + * @throws Exception + */ + public static function updateAppByOCSId($ocsId) { + $ocsClient = new OCSClient( + \OC::$server->getHTTPClientService(), + \OC::$server->getConfig(), + \OC::$server->getLogger() + ); + $appData = $ocsClient->getApplication($ocsId, \OCP\Util::getVersion()); + $download = $ocsClient->getApplicationDownload($ocsId, \OCP\Util::getVersion()); + + if (isset($download['downloadlink']) && trim($download['downloadlink']) !== '') { + $download['downloadlink'] = str_replace(' ', '%20', $download['downloadlink']); + $info = array( + 'source' => 'http', + 'href' => $download['downloadlink'], + 'appdata' => $appData + ); + } else { + throw new \Exception('Could not fetch app info!'); + } + + return self::updateApp($info); + } + + /** + * @param array $data + * @return array + * @throws Exception + */ + public static function downloadApp($data = array()) { + $l = \OC::$server->getL10N('lib'); + + if(!isset($data['source'])) { + throw new \Exception($l->t("No source specified when installing app")); + } + + //download the file if necessary + if($data['source']=='http') { + $pathInfo = pathinfo($data['href']); + $extension = isset($pathInfo['extension']) ? '.' . $pathInfo['extension'] : ''; + $path = \OC::$server->getTempManager()->getTemporaryFile($extension); + if(!isset($data['href'])) { + throw new \Exception($l->t("No href specified when installing app from http")); + } + $client = \OC::$server->getHTTPClientService()->newClient(); + $client->get($data['href'], ['save_to' => $path]); + } else { + if(!isset($data['path'])) { + throw new \Exception($l->t("No path specified when installing app from local file")); + } + $path=$data['path']; + } + + //detect the archive type + $mime = \OC::$server->getMimeTypeDetector()->detect($path); + if ($mime !=='application/zip' && $mime !== 'application/x-gzip' && $mime !== 'application/x-bzip2') { + throw new \Exception($l->t("Archives of type %s are not supported", array($mime))); + } + + //extract the archive in a temporary folder + $extractDir = \OC::$server->getTempManager()->getTemporaryFolder(); + OC_Helper::rmdirr($extractDir); + mkdir($extractDir); + if($archive=OC_Archive::open($path)) { + $archive->extract($extractDir); + } else { + OC_Helper::rmdirr($extractDir); + if($data['source']=='http') { + unlink($path); + } + throw new \Exception($l->t("Failed to open archive when installing app")); + } + + return array( + $extractDir, + $path + ); + } + + /** + * check an app's integrity + * @param array $data + * @param string $extractDir + * @param string $path + * @param bool $isShipped + * @return array + * @throws \Exception + */ + public static function checkAppsIntegrity($data, $extractDir, $path, $isShipped = false) { + $l = \OC::$server->getL10N('lib'); + //load the info.xml file of the app + if(!is_file($extractDir.'/appinfo/info.xml')) { + //try to find it in a subdir + $dh=opendir($extractDir); + if(is_resource($dh)) { + while (($folder = readdir($dh)) !== false) { + if($folder[0]!='.' and is_dir($extractDir.'/'.$folder)) { + if(is_file($extractDir.'/'.$folder.'/appinfo/info.xml')) { + $extractDir.='/'.$folder; + } + } + } + } + } + if(!is_file($extractDir.'/appinfo/info.xml')) { + OC_Helper::rmdirr($extractDir); + if($data['source'] === 'http') { + unlink($path); + } + throw new \Exception($l->t("App does not provide an info.xml file")); + } + + $info = OC_App::getAppInfo($extractDir.'/appinfo/info.xml', true); + if(!is_array($info)) { + throw new \Exception($l->t('App cannot be installed because appinfo file cannot be read.')); + } + + // We can't trust the parsed info.xml file as it may have been tampered + // with by an attacker and thus we need to use the local data to check + // whether the application needs to be signed. + $appId = OC_App::cleanAppId($data['appdata']['id']); + $appBelongingToId = OC_App::getInternalAppIdByOcs($appId); + if(is_string($appBelongingToId)) { + $previouslySigned = \OC::$server->getConfig()->getAppValue($appBelongingToId, 'signed', 'false'); + } else { + $appBelongingToId = $info['id']; + $previouslySigned = 'false'; + } + if($data['appdata']['level'] === OC_App::officialApp || $previouslySigned === 'true') { + \OC::$server->getConfig()->setAppValue($appBelongingToId, 'signed', 'true'); + $integrityResult = \OC::$server->getIntegrityCodeChecker()->verifyAppSignature( + $appBelongingToId, + $extractDir + ); + if($integrityResult !== []) { + $e = new \Exception( + $l->t( + 'Signature could not get checked. Please contact the app developer and check your admin screen.' + ) + ); + throw $e; + } + } + + // check the code for not allowed calls + if(!$isShipped && !OC_Installer::checkCode($extractDir)) { + OC_Helper::rmdirr($extractDir); + throw new \Exception($l->t("App can't be installed because of not allowed code in the App")); + } + + // check if the app is compatible with this version of ownCloud + if(!OC_App::isAppCompatible(\OCP\Util::getVersion(), $info)) { + OC_Helper::rmdirr($extractDir); + throw new \Exception($l->t("App can't be installed because it is not compatible with this version of ownCloud")); + } + + // check if shipped tag is set which is only allowed for apps that are shipped with ownCloud + if(!$isShipped && isset($info['shipped']) && ($info['shipped']=='true')) { + OC_Helper::rmdirr($extractDir); + throw new \Exception($l->t("App can't be installed because it contains the <shipped>true</shipped> tag which is not allowed for non shipped apps")); + } + + // check if the ocs version is the same as the version in info.xml/version + $version = trim($info['version']); + + if(isset($data['appdata']['version']) && $version<>trim($data['appdata']['version'])) { + OC_Helper::rmdirr($extractDir); + throw new \Exception($l->t("App can't be installed because the version in info.xml is not the same as the version reported from the app store")); + } + + return $info; + } + + /** + * Check if an update for the app is available + * @param string $app + * @return string|false false or the version number of the update + * + * The function will check if an update for a version is available + */ + public static function isUpdateAvailable( $app ) { + static $isInstanceReadyForUpdates = null; + + if ($isInstanceReadyForUpdates === null) { + $installPath = OC_App::getInstallPath(); + if ($installPath === false || $installPath === null) { + $isInstanceReadyForUpdates = false; + } else { + $isInstanceReadyForUpdates = true; + } + } + + if ($isInstanceReadyForUpdates === false) { + return false; + } + + $ocsid=\OC::$server->getAppConfig()->getValue( $app, 'ocsid', ''); + + if($ocsid<>'') { + $ocsClient = new OCSClient( + \OC::$server->getHTTPClientService(), + \OC::$server->getConfig(), + \OC::$server->getLogger() + ); + $ocsdata = $ocsClient->getApplication($ocsid, \OCP\Util::getVersion()); + $ocsversion= (string) $ocsdata['version']; + $currentversion=OC_App::getAppVersion($app); + if (version_compare($ocsversion, $currentversion, '>')) { + return($ocsversion); + }else{ + return false; + } + + }else{ + return false; + } + + } + + /** + * Check if app is already downloaded + * @param string $name name of the application to remove + * @return boolean + * + * The function will check if the app is already downloaded in the apps repository + */ + public static function isDownloaded( $name ) { + foreach(OC::$APPSROOTS as $dir) { + $dirToTest = $dir['path']; + $dirToTest .= '/'; + $dirToTest .= $name; + $dirToTest .= '/'; + + if (is_dir($dirToTest)) { + return true; + } + } + + return false; + } + + /** + * Removes an app + * @param string $name name of the application to remove + * @param array $options options + * @return boolean + * + * This function removes an app. $options is an associative array. The + * following keys are optional:ja + * - keeppreferences: boolean, if true the user preferences won't be deleted + * - keepappconfig: boolean, if true the config will be kept + * - keeptables: boolean, if true the database will be kept + * - keepfiles: boolean, if true the user files will be kept + * + * This function works as follows + * -# including appinfo/remove.php + * -# removing the files + * + * The function will not delete preferences, tables and the configuration, + * this has to be done by the function oc_app_uninstall(). + */ + public static function removeApp( $name, $options = array()) { + + if(isset($options['keeppreferences']) and $options['keeppreferences']==false ) { + // todo + // remove preferences + } + + if(isset($options['keepappconfig']) and $options['keepappconfig']==false ) { + // todo + // remove app config + } + + if(isset($options['keeptables']) and $options['keeptables']==false ) { + // todo + // remove app database tables + } + + if(isset($options['keepfiles']) and $options['keepfiles']==false ) { + // todo + // remove user files + } + + if(OC_Installer::isDownloaded( $name )) { + $appdir=OC_App::getInstallPath().'/'.$name; + OC_Helper::rmdirr($appdir); + + return true; + }else{ + \OCP\Util::writeLog('core', 'can\'t remove app '.$name.'. It is not installed.', \OCP\Util::ERROR); + + return false; + } + + } + + /** + * Installs shipped apps + * + * This function installs all apps found in the 'apps' directory that should be enabled by default; + * @param bool $softErrors When updating we ignore errors and simply log them, better to have a + * working ownCloud at the end instead of an aborted update. + * @return array Array of error messages (appid => Exception) + */ + public static function installShippedApps($softErrors = false) { + $errors = []; + foreach(OC::$APPSROOTS as $app_dir) { + if($dir = opendir( $app_dir['path'] )) { + while( false !== ( $filename = readdir( $dir ))) { + if( substr( $filename, 0, 1 ) != '.' and is_dir($app_dir['path']."/$filename") ) { + if( file_exists( $app_dir['path']."/$filename/appinfo/info.xml" )) { + if(!OC_Installer::isInstalled($filename)) { + $info=OC_App::getAppInfo($filename); + $enabled = isset($info['default_enable']); + if (($enabled || in_array($filename, \OC::$server->getAppManager()->getAlwaysEnabledApps())) + && \OC::$server->getConfig()->getAppValue($filename, 'enabled') !== 'no') { + if ($softErrors) { + try { + OC_Installer::installShippedApp($filename); + } catch (\Doctrine\DBAL\Exception\TableExistsException $e) { + $errors[$filename] = $e; + continue; + } + } else { + OC_Installer::installShippedApp($filename); + } + \OC::$server->getConfig()->setAppValue($filename, 'enabled', 'yes'); + } + } + } + } + } + closedir( $dir ); + } + } + + return $errors; + } + + /** + * install an app already placed in the app folder + * @param string $app id of the app to install + * @return integer + */ + public static function installShippedApp($app) { + //install the database + $appPath = OC_App::getAppPath($app); + if(is_file("$appPath/appinfo/database.xml")) { + OC_DB::createDbFromStructure("$appPath/appinfo/database.xml"); + } + + //run appinfo/install.php + \OC::$loader->addValidRoot($appPath); + self::includeAppScript("$appPath/appinfo/install.php"); + + $info = OC_App::getAppInfo($app); + if (is_null($info)) { + return false; + } + + $config = \OC::$server->getConfig(); + + $config->setAppValue($app, 'installed_version', OC_App::getAppVersion($app)); + if (array_key_exists('ocsid', $info)) { + $config->setAppValue($app, 'ocsid', $info['ocsid']); + } + + //set remote/public handlers + foreach($info['remote'] as $name=>$path) { + $config->setAppValue('core', 'remote_'.$name, $app.'/'.$path); + } + foreach($info['public'] as $name=>$path) { + $config->setAppValue('core', 'public_'.$name, $app.'/'.$path); + } + + OC_App::setAppTypes($info['id']); + + return $info['id']; + } + + /** + * check the code of an app with some static code checks + * @param string $folder the folder of the app to check + * @return boolean true for app is o.k. and false for app is not o.k. + */ + public static function checkCode($folder) { + // is the code checker enabled? + if(!\OC::$server->getConfig()->getSystemValue('appcodechecker', false)) { + return true; + } + + $codeChecker = new CodeChecker(new PrivateCheck(new EmptyCheck())); + $errors = $codeChecker->analyseFolder($folder); + + return empty($errors); + } + + /** + * @param $basedir + */ + private static function includeAppScript($script) { + if ( file_exists($script) ){ + include $script; + } + } +} diff --git a/lib/private/legacy/json.php b/lib/private/legacy/json.php new file mode 100644 index 00000000000..74aebd476fb --- /dev/null +++ b/lib/private/legacy/json.php @@ -0,0 +1,179 @@ +<?php +/** + * @author Bart Visscher <bartv@thisnet.nl> + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Felix Moeller <mail@felixmoeller.de> + * @author Georg Ehrke <georg@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 Thomas Tanghus <thomas@tanghus.net> + * @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/> + * + */ + +/** + * Class OC_JSON + * @deprecated Use a AppFramework JSONResponse instead + */ +class OC_JSON{ + static protected $send_content_type_header = false; + /** + * set Content-Type header to jsonrequest + * @deprecated Use a AppFramework JSONResponse instead + */ + public static function setContentTypeHeader($type='application/json') { + if (!self::$send_content_type_header) { + // We send json data + header( 'Content-Type: '.$type . '; charset=utf-8'); + self::$send_content_type_header = true; + } + } + + /** + * Check if the app is enabled, send json error msg if not + * @param string $app + * @deprecated Use the AppFramework instead. It will automatically check if the app is enabled. + */ + public static function checkAppEnabled($app) { + if( !OC_App::isEnabled($app)) { + $l = \OC::$server->getL10N('lib'); + self::error(array( 'data' => array( 'message' => $l->t('Application is not enabled'), 'error' => 'application_not_enabled' ))); + exit(); + } + } + + /** + * Check if the user is logged in, send json error msg if not + * @deprecated Use annotation based ACLs from the AppFramework instead + */ + public static function checkLoggedIn() { + if( !OC_User::isLoggedIn()) { + $l = \OC::$server->getL10N('lib'); + http_response_code(\OCP\AppFramework\Http::STATUS_UNAUTHORIZED); + self::error(array( 'data' => array( 'message' => $l->t('Authentication error'), 'error' => 'authentication_error' ))); + exit(); + } + } + + /** + * Check an ajax get/post call if the request token is valid, send json error msg if not. + * @deprecated Use annotation based CSRF checks from the AppFramework instead + */ + public static function callCheck() { + if( !(\OC::$server->getRequest()->passesCSRFCheck())) { + $l = \OC::$server->getL10N('lib'); + self::error(array( 'data' => array( 'message' => $l->t('Token expired. Please reload page.'), 'error' => 'token_expired' ))); + exit(); + } + } + + /** + * Check if the user is a admin, send json error msg if not. + * @deprecated Use annotation based ACLs from the AppFramework instead + */ + public static function checkAdminUser() { + if( !OC_User::isAdminUser(OC_User::getUser())) { + $l = \OC::$server->getL10N('lib'); + self::error(array( 'data' => array( 'message' => $l->t('Authentication error'), 'error' => 'authentication_error' ))); + exit(); + } + } + + /** + * Check is a given user exists - send json error msg if not + * @param string $user + * @deprecated Use a AppFramework JSONResponse instead + */ + public static function checkUserExists($user) { + if (!OCP\User::userExists($user)) { + $l = \OC::$server->getL10N('lib'); + OCP\JSON::error(array('data' => array('message' => $l->t('Unknown user'), 'error' => 'unknown_user' ))); + exit; + } + } + + + /** + * Check if the user is a subadmin, send json error msg if not + * @deprecated Use annotation based ACLs from the AppFramework instead + */ + public static function checkSubAdminUser() { + $userObject = \OC::$server->getUserSession()->getUser(); + $isSubAdmin = false; + if($userObject !== null) { + $isSubAdmin = \OC::$server->getGroupManager()->getSubAdmin()->isSubAdmin($userObject); + } + + if(!$isSubAdmin) { + $l = \OC::$server->getL10N('lib'); + self::error(array( 'data' => array( 'message' => $l->t('Authentication error'), 'error' => 'authentication_error' ))); + exit(); + } + } + + /** + * Send json error msg + * @deprecated Use a AppFramework JSONResponse instead + */ + public static function error($data = array()) { + $data['status'] = 'error'; + self::encodedPrint($data); + } + + /** + * Send json success msg + * @deprecated Use a AppFramework JSONResponse instead + */ + public static function success($data = array()) { + $data['status'] = 'success'; + self::encodedPrint($data); + } + + /** + * Convert OC_L10N_String to string, for use in json encodings + */ + protected static function to_string(&$value) { + if ($value instanceof OC_L10N_String) { + $value = (string)$value; + } + } + + /** + * Encode and print $data in json format + * @deprecated Use a AppFramework JSONResponse instead + */ + public static function encodedPrint($data, $setContentType=true) { + if($setContentType) { + self::setContentTypeHeader(); + } + echo self::encode($data); + } + + /** + * Encode JSON + * @deprecated Use a AppFramework JSONResponse instead + */ + public static function encode($data) { + if (is_array($data)) { + array_walk_recursive($data, array('OC_JSON', 'to_string')); + } + return json_encode($data, JSON_HEX_TAG); + } +} diff --git a/lib/private/legacy/ocs.php b/lib/private/legacy/ocs.php new file mode 100644 index 00000000000..f48879a98b1 --- /dev/null +++ b/lib/private/legacy/ocs.php @@ -0,0 +1,42 @@ +<?php +/** + * @author Bart Visscher <bartv@thisnet.nl> + * @author Frank Karlitschek <frank@owncloud.org> + * @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> + * + * @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/> + * + */ +use OCP\API; + +/** + * Class to handle open collaboration services API requests + */ +class OC_OCS { + /** + * Called when a not existing OCS endpoint has been called + */ + public static function notFound() { + $format = \OC::$server->getRequest()->getParam('format', 'xml'); + $txt='Invalid query, please check the syntax. API specifications are here:' + .' http://www.freedesktop.org/wiki/Specifications/open-collaboration-services. DEBUG OUTPUT:'."\n"; + OC_API::respond(new OC_OCS_Result(null, API::RESPOND_UNKNOWN_ERROR, $txt), $format); + } + +} diff --git a/lib/private/legacy/response.php b/lib/private/legacy/response.php new file mode 100644 index 00000000000..51e0ff75e6a --- /dev/null +++ b/lib/private/legacy/response.php @@ -0,0 +1,268 @@ +<?php +/** + * @author Andreas Fischer <bantu@owncloud.com> + * @author Bart Visscher <bartv@thisnet.nl> + * @author Jรถrn Friedrich Dreyer <jfd@butonic.de> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @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/> + * + */ + +class OC_Response { + const STATUS_FOUND = 304; + const STATUS_NOT_MODIFIED = 304; + const STATUS_TEMPORARY_REDIRECT = 307; + const STATUS_BAD_REQUEST = 400; + const STATUS_NOT_FOUND = 404; + const STATUS_INTERNAL_SERVER_ERROR = 500; + const STATUS_SERVICE_UNAVAILABLE = 503; + + /** + * Enable response caching by sending correct HTTP headers + * @param integer $cache_time time to cache the response + * >0 cache time in seconds + * 0 and <0 enable default browser caching + * null cache indefinitely + */ + static public function enableCaching($cache_time = null) { + if (is_numeric($cache_time)) { + header('Pragma: public');// enable caching in IE + if ($cache_time > 0) { + self::setExpiresHeader('PT'.$cache_time.'S'); + header('Cache-Control: max-age='.$cache_time.', must-revalidate'); + } + else { + self::setExpiresHeader(0); + header("Cache-Control: must-revalidate, post-check=0, pre-check=0"); + } + } + else { + header('Cache-Control: cache'); + header('Pragma: cache'); + } + + } + + /** + * disable browser caching + * @see enableCaching with cache_time = 0 + */ + static public function disableCaching() { + self::enableCaching(0); + } + + /** + * Set response status + * @param int $status a HTTP status code, see also the STATUS constants + */ + static public function setStatus($status) { + $protocol = \OC::$server->getRequest()->getHttpProtocol(); + switch($status) { + case self::STATUS_NOT_MODIFIED: + $status = $status . ' Not Modified'; + break; + case self::STATUS_TEMPORARY_REDIRECT: + if ($protocol == 'HTTP/1.1') { + $status = $status . ' Temporary Redirect'; + break; + } else { + $status = self::STATUS_FOUND; + // fallthrough + } + case self::STATUS_FOUND; + $status = $status . ' Found'; + break; + case self::STATUS_NOT_FOUND; + $status = $status . ' Not Found'; + break; + case self::STATUS_INTERNAL_SERVER_ERROR; + $status = $status . ' Internal Server Error'; + break; + case self::STATUS_SERVICE_UNAVAILABLE; + $status = $status . ' Service Unavailable'; + break; + } + header($protocol.' '.$status); + } + + /** + * Send redirect response + * @param string $location to redirect to + */ + static public function redirect($location) { + self::setStatus(self::STATUS_TEMPORARY_REDIRECT); + header('Location: '.$location); + } + + /** + * Set response expire time + * @param string|DateTime $expires date-time when the response expires + * string for DateInterval from now + * DateTime object when to expire response + */ + static public function setExpiresHeader($expires) { + if (is_string($expires) && $expires[0] == 'P') { + $interval = $expires; + $expires = new DateTime('now'); + $expires->add(new DateInterval($interval)); + } + if ($expires instanceof DateTime) { + $expires->setTimezone(new DateTimeZone('GMT')); + $expires = $expires->format(DateTime::RFC2822); + } + header('Expires: '.$expires); + } + + /** + * Checks and set ETag header, when the request matches sends a + * 'not modified' response + * @param string $etag token to use for modification check + */ + static public function setETagHeader($etag) { + if (empty($etag)) { + return; + } + $etag = '"'.$etag.'"'; + if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && + trim($_SERVER['HTTP_IF_NONE_MATCH']) == $etag) { + self::setStatus(self::STATUS_NOT_MODIFIED); + exit; + } + header('ETag: '.$etag); + } + + /** + * Checks and set Last-Modified header, when the request matches sends a + * 'not modified' response + * @param int|DateTime|string $lastModified time when the response was last modified + */ + static public function setLastModifiedHeader($lastModified) { + if (empty($lastModified)) { + return; + } + if (is_int($lastModified)) { + $lastModified = gmdate(DateTime::RFC2822, $lastModified); + } + if ($lastModified instanceof DateTime) { + $lastModified = $lastModified->format(DateTime::RFC2822); + } + if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && + trim($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModified) { + self::setStatus(self::STATUS_NOT_MODIFIED); + exit; + } + header('Last-Modified: '.$lastModified); + } + + /** + * Sets the content disposition header (with possible workarounds) + * @param string $filename file name + * @param string $type disposition type, either 'attachment' or 'inline' + */ + static public function setContentDispositionHeader( $filename, $type = 'attachment' ) { + if (\OC::$server->getRequest()->isUserAgent( + [ + \OC\AppFramework\Http\Request::USER_AGENT_IE, + \OC\AppFramework\Http\Request::USER_AGENT_ANDROID_MOBILE_CHROME, + \OC\AppFramework\Http\Request::USER_AGENT_FREEBOX, + ])) { + header( 'Content-Disposition: ' . rawurlencode($type) . '; filename="' . rawurlencode( $filename ) . '"' ); + } else { + header( 'Content-Disposition: ' . rawurlencode($type) . '; filename*=UTF-8\'\'' . rawurlencode( $filename ) + . '; filename="' . rawurlencode( $filename ) . '"' ); + } + } + + /** + * Sets the content length header (with possible workarounds) + * @param string|int|float $length Length to be sent + */ + static public function setContentLengthHeader($length) { + if (PHP_INT_SIZE === 4) { + if ($length > PHP_INT_MAX && stripos(PHP_SAPI, 'apache') === 0) { + // Apache PHP SAPI casts Content-Length headers to PHP integers. + // This enforces a limit of PHP_INT_MAX (2147483647 on 32-bit + // platforms). So, if the length is greater than PHP_INT_MAX, + // we just do not send a Content-Length header to prevent + // bodies from being received incompletely. + return; + } + // Convert signed integer or float to unsigned base-10 string. + $lfh = new \OC\LargeFileHelper; + $length = $lfh->formatUnsignedInteger($length); + } + header('Content-Length: '.$length); + } + + /** + * Send file as response, checking and setting caching headers + * @param string $filepath of file to send + * @deprecated 8.1.0 - Use \OCP\AppFramework\Http\StreamResponse or another AppFramework controller instead + */ + static public function sendFile($filepath) { + $fp = fopen($filepath, 'rb'); + if ($fp) { + self::setLastModifiedHeader(filemtime($filepath)); + self::setETagHeader(md5_file($filepath)); + + self::setContentLengthHeader(filesize($filepath)); + fpassthru($fp); + } + else { + self::setStatus(self::STATUS_NOT_FOUND); + } + } + + /** + * This function adds some security related headers to all requests served via base.php + * The implementation of this function has to happen here to ensure that all third-party + * components (e.g. SabreDAV) also benefit from this headers. + */ + public static function addSecurityHeaders() { + /** + * FIXME: Content Security Policy for legacy ownCloud components. This + * can be removed once \OCP\AppFramework\Http\Response from the AppFramework + * is used everywhere. + * @see \OCP\AppFramework\Http\Response::getHeaders + */ + $policy = 'default-src \'self\'; ' + . 'script-src \'self\' \'unsafe-eval\'; ' + . 'style-src \'self\' \'unsafe-inline\'; ' + . 'frame-src *; ' + . 'img-src * data: blob:; ' + . 'font-src \'self\' data:; ' + . 'media-src *; ' + . 'connect-src *'; + header('Content-Security-Policy:' . $policy); + + // Send fallback headers for installations that don't have the possibility to send + // custom headers on the webserver side + if(getenv('modHeadersAvailable') !== 'true') { + header('X-XSS-Protection: 1; mode=block'); // Enforce browser based XSS filters + header('X-Content-Type-Options: nosniff'); // Disable sniffing the content type for IE + header('X-Frame-Options: Sameorigin'); // Disallow iFraming from other domains + header('X-Robots-Tag: none'); // https://developers.google.com/webmasters/control-crawl-index/docs/robots_meta_tag + header('X-Download-Options: noopen'); // https://msdn.microsoft.com/en-us/library/jj542450(v=vs.85).aspx + header('X-Permitted-Cross-Domain-Policies: none'); // https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html + } + } + +} diff --git a/lib/private/legacy/template.php b/lib/private/legacy/template.php new file mode 100644 index 00000000000..84b963e4ab6 --- /dev/null +++ b/lib/private/legacy/template.php @@ -0,0 +1,433 @@ +<?php +/** + * @author Adam Williamson <awilliam@redhat.com> + * @author Bart Visscher <bartv@thisnet.nl> + * @author Bjรถrn Schieรle <schiessle@owncloud.com> + * @author Brice Maron <brice@bmaron.net> + * @author Frank Karlitschek <frank@owncloud.org> + * @author Hendrik Leppelsack <hendrik@leppelsack.de> + * @author Individual IT Services <info@individual-it.net> + * @author Jakob Sack <mail@jakobsack.de> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Jรถrn Friedrich Dreyer <jfd@butonic.de> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Raghu Nayyar <hey@raghunayyar.com> + * @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/> + * + */ + +use OC\TemplateLayout; + +require_once __DIR__.'/../template/functions.php'; + +/** + * This class provides the templates for ownCloud. + */ +class OC_Template extends \OC\Template\Base { + + /** @var string */ + private $renderAs; // Create a full page? + + /** @var string */ + private $path; // The path to the template + + /** @var array */ + private $headers = array(); //custom headers + + /** @var string */ + protected $app; // app id + + protected static $initTemplateEngineFirstRun = true; + + /** + * Constructor + * + * @param string $app app providing the template + * @param string $name of the template file (without suffix) + * @param string $renderAs If $renderAs is set, OC_Template will try to + * produce a full page in the according layout. For + * now, $renderAs can be set to "guest", "user" or + * "admin". + * @param bool $registerCall = true + */ + public function __construct( $app, $name, $renderAs = "", $registerCall = true ) { + // Read the selected theme from the config file + self::initTemplateEngine($renderAs); + + $theme = OC_Util::getTheme(); + + $requestToken = (OC::$server->getSession() && $registerCall) ? \OCP\Util::callRegister() : ''; + + $parts = explode('/', $app); // fix translation when app is something like core/lostpassword + $l10n = \OC::$server->getL10N($parts[0]); + $themeDefaults = new OC_Defaults(); + + list($path, $template) = $this->findTemplate($theme, $app, $name); + + // Set the private data + $this->renderAs = $renderAs; + $this->path = $path; + $this->app = $app; + + parent::__construct($template, $requestToken, $l10n, $themeDefaults); + } + + /** + * @param string $renderAs + */ + public static function initTemplateEngine($renderAs) { + if (self::$initTemplateEngineFirstRun){ + + //apps that started before the template initialization can load their own scripts/styles + //so to make sure this scripts/styles here are loaded first we use OC_Util::addScript() with $prepend=true + //meaning the last script/style in this list will be loaded first + if (\OC::$server->getSystemConfig()->getValue ('installed', false) && $renderAs !== 'error' && !\OCP\Util::needUpgrade()) { + if (\OC::$server->getConfig ()->getAppValue ( 'core', 'backgroundjobs_mode', 'ajax' ) == 'ajax') { + OC_Util::addScript ( 'backgroundjobs', null, true ); + } + } + + OC_Util::addStyle("tooltip",null,true); + OC_Util::addStyle('jquery-ui-fixes',null,true); + OC_Util::addVendorStyle('jquery-ui/themes/base/jquery-ui',null,true); + OC_Util::addStyle("mobile",null,true); + OC_Util::addStyle("multiselect",null,true); + OC_Util::addStyle("fixes",null,true); + OC_Util::addStyle("global",null,true); + OC_Util::addStyle("apps",null,true); + OC_Util::addStyle("fonts",null,true); + OC_Util::addStyle("icons",null,true); + OC_Util::addStyle("header",null,true); + OC_Util::addStyle("inputs",null,true); + OC_Util::addStyle("styles",null,true); + + // avatars + if (\OC::$server->getSystemConfig()->getValue('enable_avatars', true) === true) { + \OC_Util::addScript('jquery.avatar', null, true); + \OC_Util::addScript('placeholder', null, true); + } + + OC_Util::addScript('oc-backbone', null, true); + OC_Util::addVendorScript('core', 'backbone/backbone', true); + OC_Util::addVendorScript('snapjs/dist/latest/snap', null, true); + OC_Util::addScript('mimetypelist', null, true); + OC_Util::addScript('mimetype', null, true); + OC_Util::addScript("apps", null, true); + OC_Util::addScript("oc-requesttoken", null, true); + OC_Util::addScript('search', 'search', true); + OC_Util::addScript("config", null, true); + OC_Util::addScript("eventsource", null, true); + OC_Util::addScript("octemplate", null, true); + OC_Util::addTranslations("core", null, true); + OC_Util::addScript("l10n", null, true); + OC_Util::addScript("js", null, true); + OC_Util::addScript("oc-dialogs", null, true); + OC_Util::addScript("jquery.ocdialog", null, true); + OC_Util::addStyle("jquery.ocdialog"); + OC_Util::addScript("compatibility", null, true); + OC_Util::addScript("placeholders", null, true); + OC_Util::addScript('files/fileinfo'); + OC_Util::addScript('files/client'); + + // Add the stuff we need always + // following logic will import all vendor libraries that are + // specified in core/js/core.json + $fileContent = file_get_contents(OC::$SERVERROOT . '/core/js/core.json'); + if($fileContent !== false) { + $coreDependencies = json_decode($fileContent, true); + foreach(array_reverse($coreDependencies['vendor']) as $vendorLibrary) { + // remove trailing ".js" as addVendorScript will append it + OC_Util::addVendorScript( + substr($vendorLibrary, 0, strlen($vendorLibrary) - 3),null,true); + } + } else { + throw new \Exception('Cannot read core/js/core.json'); + } + + if (\OC::$server->getRequest()->isUserAgent([\OC\AppFramework\Http\Request::USER_AGENT_IE])) { + // polyfill for btoa/atob for IE friends + OC_Util::addVendorScript('base64/base64'); + // shim for the davclient.js library + \OCP\Util::addScript('files/iedavclient'); + } + + self::$initTemplateEngineFirstRun = false; + } + + } + + + /** + * find the template with the given name + * @param string $name of the template file (without suffix) + * + * Will select the template file for the selected theme. + * Checking all the possible locations. + * @param string $theme + * @param string $app + * @return string[] + */ + protected function findTemplate($theme, $app, $name) { + // Check if it is a app template or not. + if( $app !== '' ) { + $dirs = $this->getAppTemplateDirs($theme, $app, OC::$SERVERROOT, OC_App::getAppPath($app)); + } else { + $dirs = $this->getCoreTemplateDirs($theme, OC::$SERVERROOT); + } + $locator = new \OC\Template\TemplateFileLocator( $dirs ); + $template = $locator->find($name); + $path = $locator->getPath(); + return array($path, $template); + } + + /** + * Add a custom element to the header + * @param string $tag tag name of the element + * @param array $attributes array of attributes for the element + * @param string $text the text content for the element. If $text is null then the + * element will be written as empty element. So use "" to get a closing tag. + */ + public function addHeader($tag, $attributes, $text=null) { + $this->headers[]= array( + 'tag' => $tag, + 'attributes' => $attributes, + 'text' => $text + ); + } + + /** + * Process the template + * @return boolean|string + * + * This function process the template. If $this->renderAs is set, it + * will produce a full page. + */ + public function fetchPage($additionalParams = null) { + $data = parent::fetchPage($additionalParams); + + if( $this->renderAs ) { + $page = new TemplateLayout($this->renderAs, $this->app); + + // Add custom headers + $headers = ''; + foreach(OC_Util::$headers as $header) { + $headers .= '<'.\OCP\Util::sanitizeHTML($header['tag']); + foreach($header['attributes'] as $name=>$value) { + $headers .= ' '.\OCP\Util::sanitizeHTML($name).'="'.\OCP\Util::sanitizeHTML($value).'"'; + } + if ($header['text'] !== null) { + $headers .= '>'.\OCP\Util::sanitizeHTML($header['text']).'</'.\OCP\Util::sanitizeHTML($header['tag']).'>'; + } else { + $headers .= '/>'; + } + } + + $page->assign('headers', $headers); + + $page->assign('content', $data); + return $page->fetchPage(); + } + + return $data; + } + + /** + * Include template + * + * @param string $file + * @param array|null $additionalParams + * @return string returns content of included template + * + * Includes another template. use <?php echo $this->inc('template'); ?> to + * do this. + */ + public function inc( $file, $additionalParams = null ) { + return $this->load($this->path.$file.'.php', $additionalParams); + } + + /** + * Shortcut to print a simple page for users + * @param string $application The application we render the template for + * @param string $name Name of the template + * @param array $parameters Parameters for the template + * @return boolean|null + */ + public static function printUserPage( $application, $name, $parameters = array() ) { + $content = new OC_Template( $application, $name, "user" ); + foreach( $parameters as $key => $value ) { + $content->assign( $key, $value ); + } + print $content->printPage(); + } + + /** + * Shortcut to print a simple page for admins + * @param string $application The application we render the template for + * @param string $name Name of the template + * @param array $parameters Parameters for the template + * @return bool + */ + public static function printAdminPage( $application, $name, $parameters = array() ) { + $content = new OC_Template( $application, $name, "admin" ); + foreach( $parameters as $key => $value ) { + $content->assign( $key, $value ); + } + return $content->printPage(); + } + + /** + * Shortcut to print a simple page for guests + * @param string $application The application we render the template for + * @param string $name Name of the template + * @param array|string $parameters Parameters for the template + * @return bool + */ + public static function printGuestPage( $application, $name, $parameters = array() ) { + $content = new OC_Template( $application, $name, "guest" ); + foreach( $parameters as $key => $value ) { + $content->assign( $key, $value ); + } + return $content->printPage(); + } + + /** + * Print a fatal error page and terminates the script + * @param string $error_msg The error message to show + * @param string $hint An optional hint message - needs to be properly escaped + */ + public static function printErrorPage( $error_msg, $hint = '' ) { + try { + $content = new \OC_Template( '', 'error', 'error', false ); + $errors = array(array('error' => $error_msg, 'hint' => $hint)); + $content->assign( 'errors', $errors ); + $content->printPage(); + } catch (\Exception $e) { + $logger = \OC::$server->getLogger(); + $logger->error("$error_msg $hint", ['app' => 'core']); + $logger->logException($e, ['app' => 'core']); + + header(self::getHttpProtocol() . ' 500 Internal Server Error'); + header('Content-Type: text/plain; charset=utf-8'); + print("$error_msg $hint"); + } + die(); + } + + /** + * print error page using Exception details + * @param Exception | Throwable $exception + */ + public static function printExceptionErrorPage($exception, $fetchPage = false) { + try { + $request = \OC::$server->getRequest(); + $content = new \OC_Template('', 'exception', 'error', false); + $content->assign('errorClass', get_class($exception)); + $content->assign('errorMsg', $exception->getMessage()); + $content->assign('errorCode', $exception->getCode()); + $content->assign('file', $exception->getFile()); + $content->assign('line', $exception->getLine()); + $content->assign('trace', $exception->getTraceAsString()); + $content->assign('debugMode', \OC::$server->getSystemConfig()->getValue('debug', false)); + $content->assign('remoteAddr', $request->getRemoteAddress()); + $content->assign('requestID', $request->getId()); + if ($fetchPage) { + return $content->fetchPage(); + } + $content->printPage(); + } catch (\Exception $e) { + $logger = \OC::$server->getLogger(); + $logger->logException($exception, ['app' => 'core']); + $logger->logException($e, ['app' => 'core']); + + header(self::getHttpProtocol() . ' 500 Internal Server Error'); + header('Content-Type: text/plain; charset=utf-8'); + print("Internal Server Error\n\n"); + print("The server encountered an internal error and was unable to complete your request.\n"); + print("Please contact the server administrator if this error reappears multiple times, please include the technical details below in your report.\n"); + print("More details can be found in the server log.\n"); + } + die(); + } + + /** + * This is only here to reduce the dependencies in case of an exception to + * still be able to print a plain error message. + * + * Returns the used HTTP protocol. + * + * @return string HTTP protocol. HTTP/2, HTTP/1.1 or HTTP/1.0. + * @internal Don't use this - use AppFramework\Http\Request->getHttpProtocol instead + */ + protected static function getHttpProtocol() { + $claimedProtocol = strtoupper($_SERVER['SERVER_PROTOCOL']); + $validProtocols = [ + 'HTTP/1.0', + 'HTTP/1.1', + 'HTTP/2', + ]; + if(in_array($claimedProtocol, $validProtocols, true)) { + return $claimedProtocol; + } + return 'HTTP/1.1'; + } + + /** + * @return bool + */ + public static function isAssetPipelineEnabled() { + try { + if (\OCP\Util::needUpgrade()) { + // Don't use the compiled asset when we need to do an update + return false; + } + } catch (\Exception $e) { + // Catch any exception, because this code is also called when displaying + // an exception error page. + return false; + } + + // asset management enabled? + $config = \OC::$server->getConfig(); + $useAssetPipeline = $config->getSystemValue('asset-pipeline.enabled', false); + if (!$useAssetPipeline) { + return false; + } + + // assets folder exists? + $assetDir = $config->getSystemValue('assetdirectory', \OC::$SERVERROOT) . '/assets'; + if (!is_dir($assetDir)) { + if (!mkdir($assetDir)) { + \OCP\Util::writeLog('assets', + "Folder <$assetDir> does not exist and/or could not be generated.", \OCP\Util::ERROR); + return false; + } + } + + // assets folder can be accessed? + if (!touch($assetDir."/.oc")) { + \OCP\Util::writeLog('assets', + "Folder <$assetDir> could not be accessed.", \OCP\Util::ERROR); + return false; + } + return $useAssetPipeline; + } + +} diff --git a/lib/private/legacy/user.php b/lib/private/legacy/user.php new file mode 100644 index 00000000000..11c35daa0de --- /dev/null +++ b/lib/private/legacy/user.php @@ -0,0 +1,639 @@ +<?php +/** + * @author Aldo "xoen" Giambelluca <xoen@xoen.org> + * @author Andreas Fischer <bantu@owncloud.com> + * @author Arthur Schiwon <blizzz@owncloud.com> + * @author Bart Visscher <bartv@thisnet.nl> + * @author Bartek Przybylski <bart.p.pl@gmail.com> + * @author Bjรถrn Schieรle <schiessle@owncloud.com> + * @author Florian Preinstorfer <nblock@archlinux.us> + * @author Georg Ehrke <georg@owncloud.com> + * @author Jakob Sack <mail@jakobsack.de> + * @author Jรถrn Friedrich Dreyer <jfd@butonic.de> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author Roeland Jago Douma <rullzer@owncloud.com> + * @author shkdee <louis.traynard@m4x.org> + * @author Thomas Mรผller <thomas.mueller@tmit.eu> + * @author Tom Needham <tom@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/> + * + */ + +/** + * This class provides wrapper methods for user management. Multiple backends are + * supported. User management operations are delegated to the configured backend for + * execution. + * + * Note that &run is deprecated and won't work anymore. + * + * Hooks provided: + * pre_createUser(&run, uid, password) + * post_createUser(uid, password) + * pre_deleteUser(&run, uid) + * post_deleteUser(uid) + * pre_setPassword(&run, uid, password, recoveryPassword) + * post_setPassword(uid, password, recoveryPassword) + * pre_login(&run, uid, password) + * post_login(uid) + * logout() + */ +class OC_User { + + /** + * @return \OC\User\Session + */ + public static function getUserSession() { + return OC::$server->getUserSession(); + } + + private static $_backends = array(); + + private static $_usedBackends = array(); + + private static $_setupedBackends = array(); + + // bool, stores if a user want to access a resource anonymously, e.g if he opens a public link + private static $incognitoMode = false; + + /** + * Adds the backend to the list of used backends + * + * @param string|\OCP\UserInterface $backend default: database The backend to use for user management + * @return bool + * + * Set the User Authentication Module + */ + public static function useBackend($backend = 'database') { + if ($backend instanceof \OCP\UserInterface) { + self::$_usedBackends[get_class($backend)] = $backend; + \OC::$server->getUserManager()->registerBackend($backend); + } else { + // You'll never know what happens + if (null === $backend OR !is_string($backend)) { + $backend = 'database'; + } + + // Load backend + switch ($backend) { + case 'database': + case 'mysql': + case 'sqlite': + \OCP\Util::writeLog('core', 'Adding user backend ' . $backend . '.', \OCP\Util::DEBUG); + self::$_usedBackends[$backend] = new OC_User_Database(); + \OC::$server->getUserManager()->registerBackend(self::$_usedBackends[$backend]); + break; + case 'dummy': + self::$_usedBackends[$backend] = new \Test\Util\User\Dummy(); + \OC::$server->getUserManager()->registerBackend(self::$_usedBackends[$backend]); + break; + default: + \OCP\Util::writeLog('core', 'Adding default user backend ' . $backend . '.', \OCP\Util::DEBUG); + $className = 'OC_USER_' . strToUpper($backend); + self::$_usedBackends[$backend] = new $className(); + \OC::$server->getUserManager()->registerBackend(self::$_usedBackends[$backend]); + break; + } + } + return true; + } + + /** + * remove all used backends + */ + public static function clearBackends() { + self::$_usedBackends = array(); + \OC::$server->getUserManager()->clearBackends(); + } + + /** + * setup the configured backends in config.php + */ + public static function setupBackends() { + OC_App::loadApps(array('prelogin')); + $backends = \OC::$server->getSystemConfig()->getValue('user_backends', array()); + foreach ($backends as $i => $config) { + $class = $config['class']; + $arguments = $config['arguments']; + if (class_exists($class)) { + if (array_search($i, self::$_setupedBackends) === false) { + // make a reflection object + $reflectionObj = new ReflectionClass($class); + + // use Reflection to create a new instance, using the $args + $backend = $reflectionObj->newInstanceArgs($arguments); + self::useBackend($backend); + self::$_setupedBackends[] = $i; + } else { + \OCP\Util::writeLog('core', 'User backend ' . $class . ' already initialized.', \OCP\Util::DEBUG); + } + } else { + \OCP\Util::writeLog('core', 'User backend ' . $class . ' not found.', \OCP\Util::ERROR); + } + } + } + + /** + * Try to login a user + * + * @param string $loginname The login name of the user to log in + * @param string $password The password of the user + * @return boolean|null + * + * Log in a user and regenerate a new session - if the password is ok + */ + public static function login($loginname, $password) { + $result = self::getUserSession()->login($loginname, $password); + if ($result) { + // Refresh the token + \OC::$server->getCsrfTokenManager()->refreshToken(); + //we need to pass the user name, which may differ from login name + $user = self::getUserSession()->getUser()->getUID(); + OC_Util::setupFS($user); + //trigger creation of user home and /files folder + \OC::$server->getUserFolder($user); + } + return $result; + } + + /** + * Try to login a user using the magic cookie (remember login) + * + * @param string $uid The username of the user to log in + * @param string $token + * @return bool + */ + public static function loginWithCookie($uid, $token) { + return self::getUserSession()->loginWithCookie($uid, $token); + } + + /** + * Try to login a user, assuming authentication + * has already happened (e.g. via Single Sign On). + * + * Log in a user and regenerate a new session. + * + * @param \OCP\Authentication\IApacheBackend $backend + * @return bool + */ + public static function loginWithApache(\OCP\Authentication\IApacheBackend $backend) { + + $uid = $backend->getCurrentUserId(); + $run = true; + OC_Hook::emit("OC_User", "pre_login", array("run" => &$run, "uid" => $uid)); + + if ($uid) { + if (self::getUser() !== $uid) { + self::setUserId($uid); + self::setDisplayName($uid); + self::getUserSession()->setLoginName($uid); + // setup the filesystem + OC_Util::setupFS($uid); + //trigger creation of user home and /files folder + \OC::$server->getUserFolder($uid); + + OC_Hook::emit("OC_User", "post_login", array("uid" => $uid, 'password' => '')); + } + return true; + } + return false; + } + + /** + * Verify with Apache whether user is authenticated. + * + * @return boolean|null + * true: authenticated + * false: not authenticated + * null: not handled / no backend available + */ + public static function handleApacheAuth() { + $backend = self::findFirstActiveUsedBackend(); + if ($backend) { + OC_App::loadApps(); + + //setup extra user backends + self::setupBackends(); + self::unsetMagicInCookie(); + + return self::loginWithApache($backend); + } + + return null; + } + + + /** + * Sets user id for session and triggers emit + */ + public static function setUserId($uid) { + $userSession = \OC::$server->getUserSession(); + $userManager = \OC::$server->getUserManager(); + if ($user = $userManager->get($uid)) { + $userSession->setUser($user); + } else { + \OC::$server->getSession()->set('user_id', $uid); + } + } + + /** + * Sets user display name for session + * + * @param string $uid + * @param string $displayName + * @return bool Whether the display name could get set + */ + public static function setDisplayName($uid, $displayName = null) { + if (is_null($displayName)) { + $displayName = $uid; + } + $user = \OC::$server->getUserManager()->get($uid); + if ($user) { + return $user->setDisplayName($displayName); + } else { + return false; + } + } + + /** + * Tries to login the user with HTTP Basic Authentication + */ + public static function tryBasicAuthLogin() { + if (!empty($_SERVER['PHP_AUTH_USER']) && !empty($_SERVER['PHP_AUTH_PW'])) { + $result = \OC_User::login($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']); + if($result === true) { + /** + * Add DAV authenticated. This should in an ideal world not be + * necessary but the iOS App reads cookies from anywhere instead + * only the DAV endpoint. + * This makes sure that the cookies will be valid for the whole scope + * @see https://github.com/owncloud/core/issues/22893 + */ + \OC::$server->getSession()->set( + \OCA\DAV\Connector\Sabre\Auth::DAV_AUTHENTICATED, + \OC::$server->getUserSession()->getUser()->getUID() + ); + } + } + } + + /** + * Check if the user is logged in, considers also the HTTP basic credentials + * + * @return bool + */ + public static function isLoggedIn() { + if (\OC::$server->getSession()->get('user_id') !== null && self::$incognitoMode === false) { + return self::userExists(\OC::$server->getSession()->get('user_id')); + } + + return false; + } + + /** + * set incognito mode, e.g. if a user wants to open a public link + * + * @param bool $status + */ + public static function setIncognitoMode($status) { + self::$incognitoMode = $status; + } + + /** + * get incognito mode status + * + * @return bool + */ + public static function isIncognitoMode() { + return self::$incognitoMode; + } + + /** + * Supplies an attribute to the logout hyperlink. The default behaviour + * is to return an href with '?logout=true' appended. However, it can + * supply any attribute(s) which are valid for <a>. + * + * @return string with one or more HTML attributes. + */ + public static function getLogoutAttribute() { + $backend = self::findFirstActiveUsedBackend(); + if ($backend) { + return $backend->getLogoutAttribute(); + } + + $logoutUrl = \OC::$server->getURLGenerator()->linkToRouteAbsolute( + 'core.login.logout', + [ + 'requesttoken' => \OCP\Util::callRegister(), + ] + ); + + return 'href="'.$logoutUrl.'"'; + } + + /** + * Check if the user is an admin user + * + * @param string $uid uid of the admin + * @return bool + */ + public static function isAdminUser($uid) { + if (OC_Group::inGroup($uid, 'admin') && self::$incognitoMode === false) { + return true; + } + return false; + } + + + /** + * get the user id of the user currently logged in. + * + * @return string|bool uid or false + */ + public static function getUser() { + $uid = \OC::$server->getSession() ? \OC::$server->getSession()->get('user_id') : null; + if (!is_null($uid) && self::$incognitoMode === false) { + return $uid; + } else { + return false; + } + } + + /** + * get the display name of the user currently logged in. + * + * @param string $uid + * @return string uid or false + */ + public static function getDisplayName($uid = null) { + if ($uid) { + $user = \OC::$server->getUserManager()->get($uid); + if ($user) { + return $user->getDisplayName(); + } else { + return $uid; + } + } else { + $user = self::getUserSession()->getUser(); + if ($user) { + return $user->getDisplayName(); + } else { + return false; + } + } + } + + /** + * Autogenerate a password + * + * @return string + * + * generates a password + */ + public static function generatePassword() { + return \OC::$server->getSecureRandom()->generate(30); + } + + /** + * Set password + * + * @param string $uid The username + * @param string $password The new password + * @param string $recoveryPassword for the encryption app to reset encryption keys + * @return bool + * + * Change the password of a user + */ + public static function setPassword($uid, $password, $recoveryPassword = null) { + $user = \OC::$server->getUserManager()->get($uid); + if ($user) { + return $user->setPassword($password, $recoveryPassword); + } else { + return false; + } + } + + /** + * Check whether user can change his avatar + * + * @param string $uid The username + * @return bool + * + * Check whether a specified user can change his avatar + */ + public static function canUserChangeAvatar($uid) { + $user = \OC::$server->getUserManager()->get($uid); + if ($user) { + return $user->canChangeAvatar(); + } else { + return false; + } + } + + /** + * Check whether user can change his password + * + * @param string $uid The username + * @return bool + * + * Check whether a specified user can change his password + */ + public static function canUserChangePassword($uid) { + $user = \OC::$server->getUserManager()->get($uid); + if ($user) { + return $user->canChangePassword(); + } else { + return false; + } + } + + /** + * Check whether user can change his display name + * + * @param string $uid The username + * @return bool + * + * Check whether a specified user can change his display name + */ + public static function canUserChangeDisplayName($uid) { + $user = \OC::$server->getUserManager()->get($uid); + if ($user) { + return $user->canChangeDisplayName(); + } else { + return false; + } + } + + /** + * Check if the password is correct + * + * @param string $uid The username + * @param string $password The password + * @return string|false user id a string on success, false otherwise + * + * Check if the password is correct without logging in the user + * returns the user id or false + */ + public static function checkPassword($uid, $password) { + $manager = \OC::$server->getUserManager(); + $username = $manager->checkPassword($uid, $password); + if ($username !== false) { + return $username->getUID(); + } + return false; + } + + /** + * @param string $uid The username + * @return string + * + * returns the path to the users home directory + * @deprecated Use \OC::$server->getUserManager->getHome() + */ + public static function getHome($uid) { + $user = \OC::$server->getUserManager()->get($uid); + if ($user) { + return $user->getHome(); + } else { + return \OC::$server->getSystemConfig()->getValue('datadirectory', OC::$SERVERROOT . '/data') . '/' . $uid; + } + } + + /** + * Get a list of all users + * + * @return array an array of all uids + * + * Get a list of all users. + * @param string $search + * @param integer $limit + * @param integer $offset + */ + public static function getUsers($search = '', $limit = null, $offset = null) { + $users = \OC::$server->getUserManager()->search($search, $limit, $offset); + $uids = array(); + foreach ($users as $user) { + $uids[] = $user->getUID(); + } + return $uids; + } + + /** + * Get a list of all users display name + * + * @param string $search + * @param int $limit + * @param int $offset + * @return array associative array with all display names (value) and corresponding uids (key) + * + * Get a list of all display names and user ids. + * @deprecated Use \OC::$server->getUserManager->searchDisplayName($search, $limit, $offset) instead. + */ + public static function getDisplayNames($search = '', $limit = null, $offset = null) { + $displayNames = array(); + $users = \OC::$server->getUserManager()->searchDisplayName($search, $limit, $offset); + foreach ($users as $user) { + $displayNames[$user->getUID()] = $user->getDisplayName(); + } + return $displayNames; + } + + /** + * check if a user exists + * + * @param string $uid the username + * @return boolean + */ + public static function userExists($uid) { + return \OC::$server->getUserManager()->userExists($uid); + } + + /** + * disables a user + * + * @param string $uid the user to disable + */ + public static function disableUser($uid) { + $user = \OC::$server->getUserManager()->get($uid); + if ($user) { + $user->setEnabled(false); + } + } + + /** + * enable a user + * + * @param string $uid + */ + public static function enableUser($uid) { + $user = \OC::$server->getUserManager()->get($uid); + if ($user) { + $user->setEnabled(true); + } + } + + /** + * checks if a user is enabled + * + * @param string $uid + * @return bool + */ + public static function isEnabled($uid) { + $user = \OC::$server->getUserManager()->get($uid); + if ($user) { + return $user->isEnabled(); + } else { + return false; + } + } + + /** + * Set cookie value to use in next page load + * + * @param string $username username to be set + * @param string $token + */ + public static function setMagicInCookie($username, $token) { + self::getUserSession()->setMagicInCookie($username, $token); + } + + /** + * Remove cookie for "remember username" + */ + public static function unsetMagicInCookie() { + self::getUserSession()->unsetMagicInCookie(); + } + + /** + * Returns the first active backend from self::$_usedBackends. + * + * @return OCP\Authentication\IApacheBackend|null if no backend active, otherwise OCP\Authentication\IApacheBackend + */ + private static function findFirstActiveUsedBackend() { + foreach (self::$_usedBackends as $backend) { + if ($backend instanceof OCP\Authentication\IApacheBackend) { + if ($backend->isSessionActive()) { + return $backend; + } + } + } + + return null; + } +} diff --git a/lib/private/legacy/util.php b/lib/private/legacy/util.php new file mode 100644 index 00000000000..b3432470f03 --- /dev/null +++ b/lib/private/legacy/util.php @@ -0,0 +1,1450 @@ +<?php +/** + * @author Adam Williamson <awilliam@redhat.com> + * @author Andreas Bรถhler <dev@aboehler.at> + * @author Andreas Fischer <bantu@owncloud.com> + * @author Arthur Schiwon <blizzz@owncloud.com> + * @author Bart Visscher <bartv@thisnet.nl> + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Birk Borkason <daniel.niccoli@gmail.com> + * @author Bjรถrn Schieรle <schiessle@owncloud.com> + * @author Brice Maron <brice@bmaron.net> + * @author Christopher Schรคpers <kondou@ts.unde.re> + * @author Clark Tomlinson <fallen013@gmail.com> + * @author cmeh <cmeh@users.noreply.github.com> + * @author Florin Peter <github@florin-peter.de> + * @author Frank Karlitschek <frank@owncloud.org> + * @author Georg Ehrke <georg@owncloud.com> + * @author helix84 <helix84@centrum.sk> + * @author Individual IT Services <info@individual-it.net> + * @author Jakob Sack <mail@jakobsack.de> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Jรถrn Friedrich Dreyer <jfd@butonic.de> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Markus Goetz <markus@woboq.com> + * @author Martin Mattel <martin.mattel@diemattels.at> + * @author Marvin Thomas Rabe <mrabe@marvinrabe.de> + * @author Michael Gapczynski <GapczynskiM@gmail.com> + * @author Michael Gรถhler <somebody.here@gmx.de> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author Roeland Jago Douma <rullzer@owncloud.com> + * @author Stefan Rado <owncloud@sradonia.net> + * @author Thomas Mรผller <thomas.mueller@tmit.eu> + * @author Thomas Schmidt <tschmidt@suse.de> + * @author Thomas Tanghus <thomas@tanghus.net> + * @author Victor Dubiniuk <dubiniuk@owncloud.com> + * @author Vincent Chan <plus.vincchan@gmail.com> + * @author Vincent Petry <pvince81@owncloud.com> + * @author Volkan Gezer <volkangezer@gmail.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/> + * + */ + +use OCP\IConfig; +use OCP\IGroupManager; +use OCP\IUser; + +class OC_Util { + public static $scripts = array(); + public static $styles = array(); + public static $headers = array(); + private static $rootMounted = false; + private static $fsSetup = false; + + protected static function getAppManager() { + return \OC::$server->getAppManager(); + } + + private static function initLocalStorageRootFS() { + // mount local file backend as root + $configDataDirectory = \OC::$server->getSystemConfig()->getValue("datadirectory", OC::$SERVERROOT . "/data"); + //first set up the local "root" storage + \OC\Files\Filesystem::initMountManager(); + if (!self::$rootMounted) { + \OC\Files\Filesystem::mount('\OC\Files\Storage\Local', array('datadir' => $configDataDirectory), '/'); + self::$rootMounted = true; + } + } + + /** + * mounting an object storage as the root fs will in essence remove the + * necessity of a data folder being present. + * TODO make home storage aware of this and use the object storage instead of local disk access + * + * @param array $config containing 'class' and optional 'arguments' + */ + private static function initObjectStoreRootFS($config) { + // check misconfiguration + if (empty($config['class'])) { + \OCP\Util::writeLog('files', 'No class given for objectstore', \OCP\Util::ERROR); + } + if (!isset($config['arguments'])) { + $config['arguments'] = array(); + } + + // instantiate object store implementation + $config['arguments']['objectstore'] = new $config['class']($config['arguments']); + // mount with plain / root object store implementation + $config['class'] = '\OC\Files\ObjectStore\ObjectStoreStorage'; + + // mount object storage as root + \OC\Files\Filesystem::initMountManager(); + if (!self::$rootMounted) { + \OC\Files\Filesystem::mount($config['class'], $config['arguments'], '/'); + self::$rootMounted = true; + } + } + + /** + * Can be set up + * + * @param string $user + * @return boolean + * @description configure the initial filesystem based on the configuration + */ + public static function setupFS($user = '') { + //setting up the filesystem twice can only lead to trouble + if (self::$fsSetup) { + return false; + } + + \OC::$server->getEventLogger()->start('setup_fs', 'Setup filesystem'); + + // If we are not forced to load a specific user we load the one that is logged in + if ($user === null) { + $user = ''; + } else if ($user == "" && OC_User::isLoggedIn()) { + $user = OC_User::getUser(); + } + + // load all filesystem apps before, so no setup-hook gets lost + OC_App::loadApps(array('filesystem')); + + // the filesystem will finish when $user is not empty, + // mark fs setup here to avoid doing the setup from loading + // OC_Filesystem + if ($user != '') { + self::$fsSetup = true; + } + + \OC\Files\Filesystem::initMountManager(); + + \OC\Files\Filesystem::logWarningWhenAddingStorageWrapper(false); + \OC\Files\Filesystem::addStorageWrapper('mount_options', function ($mountPoint, \OCP\Files\Storage $storage, \OCP\Files\Mount\IMountPoint $mount) { + if ($storage->instanceOfStorage('\OC\Files\Storage\Common')) { + /** @var \OC\Files\Storage\Common $storage */ + $storage->setMountOptions($mount->getOptions()); + } + return $storage; + }); + + \OC\Files\Filesystem::addStorageWrapper('enable_sharing', function ($mountPoint, \OCP\Files\Storage $storage, \OCP\Files\Mount\IMountPoint $mount) { + if (!$mount->getOption('enable_sharing', true)) { + return new \OC\Files\Storage\Wrapper\PermissionsMask([ + 'storage' => $storage, + 'mask' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_SHARE + ]); + } + return $storage; + }); + + // install storage availability wrapper, before most other wrappers + \OC\Files\Filesystem::addStorageWrapper('oc_availability', function ($mountPoint, $storage) { + if (!$storage->instanceOfStorage('\OC\Files\Storage\Shared') && !$storage->isLocal()) { + return new \OC\Files\Storage\Wrapper\Availability(['storage' => $storage]); + } + return $storage; + }); + + \OC\Files\Filesystem::addStorageWrapper('oc_quota', function ($mountPoint, $storage) { + // set up quota for home storages, even for other users + // which can happen when using sharing + + /** + * @var \OC\Files\Storage\Storage $storage + */ + if ($storage->instanceOfStorage('\OC\Files\Storage\Home') + || $storage->instanceOfStorage('\OC\Files\ObjectStore\HomeObjectStoreStorage') + ) { + /** @var \OC\Files\Storage\Home $storage */ + if (is_object($storage->getUser())) { + $user = $storage->getUser()->getUID(); + $quota = OC_Util::getUserQuota($user); + if ($quota !== \OCP\Files\FileInfo::SPACE_UNLIMITED) { + return new \OC\Files\Storage\Wrapper\Quota(array('storage' => $storage, 'quota' => $quota, 'root' => 'files')); + } + } + } + + return $storage; + }); + + OC_Hook::emit('OC_Filesystem', 'preSetup', array('user' => $user)); + \OC\Files\Filesystem::logWarningWhenAddingStorageWrapper(true); + + //check if we are using an object storage + $objectStore = \OC::$server->getSystemConfig()->getValue('objectstore', null); + if (isset($objectStore)) { + self::initObjectStoreRootFS($objectStore); + } else { + self::initLocalStorageRootFS(); + } + + if ($user != '' && !OCP\User::userExists($user)) { + \OC::$server->getEventLogger()->end('setup_fs'); + return false; + } + + //if we aren't logged in, there is no use to set up the filesystem + if ($user != "") { + + $userDir = '/' . $user . '/files'; + + //jail the user into his "home" directory + \OC\Files\Filesystem::init($user, $userDir); + + OC_Hook::emit('OC_Filesystem', 'setup', array('user' => $user, 'user_dir' => $userDir)); + } + \OC::$server->getEventLogger()->end('setup_fs'); + return true; + } + + /** + * check if a password is required for each public link + * + * @return boolean + */ + public static function isPublicLinkPasswordRequired() { + $appConfig = \OC::$server->getAppConfig(); + $enforcePassword = $appConfig->getValue('core', 'shareapi_enforce_links_password', 'no'); + return ($enforcePassword === 'yes') ? true : false; + } + + /** + * check if sharing is disabled for the current user + * @param IConfig $config + * @param IGroupManager $groupManager + * @param IUser|null $user + * @return bool + */ + public static function isSharingDisabledForUser(IConfig $config, IGroupManager $groupManager, $user) { + if ($config->getAppValue('core', 'shareapi_exclude_groups', 'no') === 'yes') { + $groupsList = $config->getAppValue('core', 'shareapi_exclude_groups_list', ''); + $excludedGroups = json_decode($groupsList); + if (is_null($excludedGroups)) { + $excludedGroups = explode(',', $groupsList); + $newValue = json_encode($excludedGroups); + $config->setAppValue('core', 'shareapi_exclude_groups_list', $newValue); + } + $usersGroups = $groupManager->getUserGroupIds($user); + if (!empty($usersGroups)) { + $remainingGroups = array_diff($usersGroups, $excludedGroups); + // if the user is only in groups which are disabled for sharing then + // sharing is also disabled for the user + if (empty($remainingGroups)) { + return true; + } + } + } + return false; + } + + /** + * check if share API enforces a default expire date + * + * @return boolean + */ + public static function isDefaultExpireDateEnforced() { + $isDefaultExpireDateEnabled = \OCP\Config::getAppValue('core', 'shareapi_default_expire_date', 'no'); + $enforceDefaultExpireDate = false; + if ($isDefaultExpireDateEnabled === 'yes') { + $value = \OCP\Config::getAppValue('core', 'shareapi_enforce_expire_date', 'no'); + $enforceDefaultExpireDate = ($value === 'yes') ? true : false; + } + + return $enforceDefaultExpireDate; + } + + /** + * Get the quota of a user + * + * @param string $user + * @return int Quota bytes + */ + public static function getUserQuota($user) { + $userQuota = \OC::$server->getUserManager()->get($user)->getQuota(); + if($userQuota === 'none') { + return \OCP\Files\FileInfo::SPACE_UNLIMITED; + }else{ + return OC_Helper::computerFileSize($userQuota); + } + } + + /** + * copies the skeleton to the users /files + * + * @param String $userId + * @param \OCP\Files\Folder $userDirectory + */ + public static function copySkeleton($userId, \OCP\Files\Folder $userDirectory) { + + $skeletonDirectory = \OCP\Config::getSystemValue('skeletondirectory', \OC::$SERVERROOT . '/core/skeleton'); + + if (!empty($skeletonDirectory)) { + \OCP\Util::writeLog( + 'files_skeleton', + 'copying skeleton for '.$userId.' from '.$skeletonDirectory.' to '.$userDirectory->getFullPath('/'), + \OCP\Util::DEBUG + ); + self::copyr($skeletonDirectory, $userDirectory); + // update the file cache + $userDirectory->getStorage()->getScanner()->scan('', \OC\Files\Cache\Scanner::SCAN_RECURSIVE); + } + } + + /** + * copies a directory recursively by using streams + * + * @param string $source + * @param \OCP\Files\Folder $target + * @return void + */ + public static function copyr($source, \OCP\Files\Folder $target) { + $dir = opendir($source); + while (false !== ($file = readdir($dir))) { + if (!\OC\Files\Filesystem::isIgnoredDir($file)) { + if (is_dir($source . '/' . $file)) { + $child = $target->newFolder($file); + self::copyr($source . '/' . $file, $child); + } else { + $child = $target->newFile($file); + stream_copy_to_stream(fopen($source . '/' . $file,'r'), $child->fopen('w')); + } + } + } + closedir($dir); + } + + /** + * @return void + */ + public static function tearDownFS() { + \OC\Files\Filesystem::tearDown(); + self::$fsSetup = false; + self::$rootMounted = false; + } + + /** + * get the current installed version of ownCloud + * + * @return array + */ + public static function getVersion() { + OC_Util::loadVersion(); + return \OC::$server->getSession()->get('OC_Version'); + } + + /** + * get the current installed version string of ownCloud + * + * @return string + */ + public static function getVersionString() { + OC_Util::loadVersion(); + return \OC::$server->getSession()->get('OC_VersionString'); + } + + /** + * @description get the current installed edition of ownCloud. There is the community + * edition that just returns an empty string and the enterprise edition + * that returns "Enterprise". + * @return string + */ + public static function getEditionString() { + if (OC_App::isEnabled('enterprise_key')) { + return "Enterprise"; + } else { + return ""; + } + + } + + /** + * @description get the update channel of the current installed of ownCloud. + * @return string + */ + public static function getChannel() { + OC_Util::loadVersion(); + return \OC::$server->getSession()->get('OC_Channel'); + } + + /** + * @description get the build number of the current installed of ownCloud. + * @return string + */ + public static function getBuild() { + OC_Util::loadVersion(); + return \OC::$server->getSession()->get('OC_Build'); + } + + /** + * @description load the version.php into the session as cache + */ + private static function loadVersion() { + $timestamp = filemtime(OC::$SERVERROOT . '/version.php'); + if (!\OC::$server->getSession()->exists('OC_Version') or OC::$server->getSession()->get('OC_Version_Timestamp') != $timestamp) { + require OC::$SERVERROOT . '/version.php'; + $session = \OC::$server->getSession(); + /** @var $timestamp int */ + $session->set('OC_Version_Timestamp', $timestamp); + /** @var $OC_Version string */ + $session->set('OC_Version', $OC_Version); + /** @var $OC_VersionString string */ + $session->set('OC_VersionString', $OC_VersionString); + /** @var $OC_Build string */ + $session->set('OC_Build', $OC_Build); + + // Allow overriding update channel + + if (\OC::$server->getSystemConfig()->getValue('installed', false)) { + $channel = \OC::$server->getAppConfig()->getValue('core', 'OC_Channel'); + } else { + /** @var $OC_Channel string */ + $channel = $OC_Channel; + } + + if (!is_null($channel)) { + $session->set('OC_Channel', $channel); + } else { + /** @var $OC_Channel string */ + $session->set('OC_Channel', $OC_Channel); + } + } + } + + /** + * generates a path for JS/CSS files. If no application is provided it will create the path for core. + * + * @param string $application application to get the files from + * @param string $directory directory within this application (css, js, vendor, etc) + * @param string $file the file inside of the above folder + * @return string the path + */ + private static function generatePath($application, $directory, $file) { + if (is_null($file)) { + $file = $application; + $application = ""; + } + if (!empty($application)) { + return "$application/$directory/$file"; + } else { + return "$directory/$file"; + } + } + + /** + * add a javascript file + * + * @param string $application application id + * @param string|null $file filename + * @param bool $prepend prepend the Script to the beginning of the list + * @return void + */ + public static function addScript($application, $file = null, $prepend = false) { + $path = OC_Util::generatePath($application, 'js', $file); + + // core js files need separate handling + if ($application !== 'core' && $file !== null) { + self::addTranslations ( $application ); + } + self::addExternalResource($application, $prepend, $path, "script"); + } + + /** + * add a javascript file from the vendor sub folder + * + * @param string $application application id + * @param string|null $file filename + * @param bool $prepend prepend the Script to the beginning of the list + * @return void + */ + public static function addVendorScript($application, $file = null, $prepend = false) { + $path = OC_Util::generatePath($application, 'vendor', $file); + self::addExternalResource($application, $prepend, $path, "script"); + } + + /** + * add a translation JS file + * + * @param string $application application id + * @param string $languageCode language code, defaults to the current language + * @param bool $prepend prepend the Script to the beginning of the list + */ + public static function addTranslations($application, $languageCode = null, $prepend = false) { + if (is_null($languageCode)) { + $languageCode = \OC_L10N::findLanguage($application); + } + if (!empty($application)) { + $path = "$application/l10n/$languageCode"; + } else { + $path = "l10n/$languageCode"; + } + self::addExternalResource($application, $prepend, $path, "script"); + } + + /** + * add a css file + * + * @param string $application application id + * @param string|null $file filename + * @param bool $prepend prepend the Style to the beginning of the list + * @return void + */ + public static function addStyle($application, $file = null, $prepend = false) { + $path = OC_Util::generatePath($application, 'css', $file); + self::addExternalResource($application, $prepend, $path, "style"); + } + + /** + * add a css file from the vendor sub folder + * + * @param string $application application id + * @param string|null $file filename + * @param bool $prepend prepend the Style to the beginning of the list + * @return void + */ + public static function addVendorStyle($application, $file = null, $prepend = false) { + $path = OC_Util::generatePath($application, 'vendor', $file); + self::addExternalResource($application, $prepend, $path, "style"); + } + + /** + * add an external resource css/js file + * + * @param string $application application id + * @param bool $prepend prepend the file to the beginning of the list + * @param string $path + * @param string $type (script or style) + * @return void + */ + private static function addExternalResource($application, $prepend, $path, $type = "script") { + + if ($type === "style") { + if (!in_array($path, self::$styles)) { + if ($prepend === true) { + array_unshift ( self::$styles, $path ); + } else { + self::$styles[] = $path; + } + } + } elseif ($type === "script") { + if (!in_array($path, self::$scripts)) { + if ($prepend === true) { + array_unshift ( self::$scripts, $path ); + } else { + self::$scripts [] = $path; + } + } + } + } + + /** + * Add a custom element to the header + * If $text is null then the element will be written as empty element. + * So use "" to get a closing tag. + * @param string $tag tag name of the element + * @param array $attributes array of attributes for the element + * @param string $text the text content for the element + */ + public static function addHeader($tag, $attributes, $text=null) { + self::$headers[] = array( + 'tag' => $tag, + 'attributes' => $attributes, + 'text' => $text + ); + } + + /** + * formats a timestamp in the "right" way + * + * @param int $timestamp + * @param bool $dateOnly option to omit time from the result + * @param DateTimeZone|string $timeZone where the given timestamp shall be converted to + * @return string timestamp + * + * @deprecated Use \OC::$server->query('DateTimeFormatter') instead + */ + public static function formatDate($timestamp, $dateOnly = false, $timeZone = null) { + if ($timeZone !== null && !$timeZone instanceof \DateTimeZone) { + $timeZone = new \DateTimeZone($timeZone); + } + + /** @var \OC\DateTimeFormatter $formatter */ + $formatter = \OC::$server->query('DateTimeFormatter'); + if ($dateOnly) { + return $formatter->formatDate($timestamp, 'long', $timeZone); + } + return $formatter->formatDateTime($timestamp, 'long', 'long', $timeZone); + } + + /** + * check if the current server configuration is suitable for ownCloud + * + * @param \OCP\IConfig $config + * @return array arrays with error messages and hints + */ + public static function checkServer(\OCP\IConfig $config) { + $l = \OC::$server->getL10N('lib'); + $errors = array(); + $CONFIG_DATADIRECTORY = $config->getSystemValue('datadirectory', OC::$SERVERROOT . '/data'); + + if (!self::needUpgrade($config) && $config->getSystemValue('installed', false)) { + // this check needs to be done every time + $errors = self::checkDataDirectoryValidity($CONFIG_DATADIRECTORY); + } + + // Assume that if checkServer() succeeded before in this session, then all is fine. + if (\OC::$server->getSession()->exists('checkServer_succeeded') && \OC::$server->getSession()->get('checkServer_succeeded')) { + return $errors; + } + + $webServerRestart = false; + $setup = new \OC\Setup($config, \OC::$server->getIniWrapper(), \OC::$server->getL10N('lib'), + new \OC_Defaults(), \OC::$server->getLogger(), \OC::$server->getSecureRandom()); + + $urlGenerator = \OC::$server->getURLGenerator(); + + $availableDatabases = $setup->getSupportedDatabases(); + if (empty($availableDatabases)) { + $errors[] = array( + 'error' => $l->t('No database drivers (sqlite, mysql, or postgresql) installed.'), + 'hint' => '' //TODO: sane hint + ); + $webServerRestart = true; + } + + // Check if server running on Windows platform + if(OC_Util::runningOnWindows()) { + $errors[] = [ + 'error' => $l->t('Microsoft Windows Platform is not supported'), + 'hint' => $l->t('Running ownCloud Server on the Microsoft Windows platform is not supported. We suggest you ' . + 'use a Linux server in a virtual machine if you have no option for migrating the server itself. ' . + 'Find Linux packages as well as easy to deploy virtual machine images on <a href="%s">%s</a>. ' . + 'For migrating existing installations to Linux you can find some tips and a migration script ' . + 'in <a href="%s">our documentation</a>.', + ['https://owncloud.org/install/', 'owncloud.org/install/', 'https://owncloud.org/?p=8045']) + ]; + } + + // Check if config folder is writable. + if(!OC_Helper::isReadOnlyConfigEnabled()) { + if (!is_writable(OC::$configDir) or !is_readable(OC::$configDir)) { + $errors[] = array( + 'error' => $l->t('Cannot write into "config" directory'), + 'hint' => $l->t('This can usually be fixed by ' + . '%sgiving the webserver write access to the config directory%s.', + array('<a href="' . $urlGenerator->linkToDocs('admin-dir_permissions') . '" target="_blank" rel="noreferrer">', '</a>')) + ); + } + } + + // Check if there is a writable install folder. + if ($config->getSystemValue('appstoreenabled', true)) { + if (OC_App::getInstallPath() === null + || !is_writable(OC_App::getInstallPath()) + || !is_readable(OC_App::getInstallPath()) + ) { + $errors[] = array( + 'error' => $l->t('Cannot write into "apps" directory'), + 'hint' => $l->t('This can usually be fixed by ' + . '%sgiving the webserver write access to the apps directory%s' + . ' or disabling the appstore in the config file.', + array('<a href="' . $urlGenerator->linkToDocs('admin-dir_permissions') . '" target="_blank" rel="noreferrer">', '</a>')) + ); + } + } + // Create root dir. + if ($config->getSystemValue('installed', false)) { + if (!is_dir($CONFIG_DATADIRECTORY)) { + $success = @mkdir($CONFIG_DATADIRECTORY); + if ($success) { + $errors = array_merge($errors, self::checkDataDirectoryPermissions($CONFIG_DATADIRECTORY)); + } else { + $errors[] = array( + 'error' => $l->t('Cannot create "data" directory (%s)', array($CONFIG_DATADIRECTORY)), + 'hint' => $l->t('This can usually be fixed by ' + . '<a href="%s" target="_blank" rel="noreferrer">giving the webserver write access to the root directory</a>.', + array($urlGenerator->linkToDocs('admin-dir_permissions'))) + ); + } + } else if (!is_writable($CONFIG_DATADIRECTORY) or !is_readable($CONFIG_DATADIRECTORY)) { + //common hint for all file permissions error messages + $permissionsHint = $l->t('Permissions can usually be fixed by ' + . '%sgiving the webserver write access to the root directory%s.', + array('<a href="' . $urlGenerator->linkToDocs('admin-dir_permissions') . '" target="_blank" rel="noreferrer">', '</a>')); + $errors[] = array( + 'error' => 'Data directory (' . $CONFIG_DATADIRECTORY . ') not writable by ownCloud', + 'hint' => $permissionsHint + ); + } else { + $errors = array_merge($errors, self::checkDataDirectoryPermissions($CONFIG_DATADIRECTORY)); + } + } + + if (!OC_Util::isSetLocaleWorking()) { + $errors[] = array( + 'error' => $l->t('Setting locale to %s failed', + array('en_US.UTF-8/fr_FR.UTF-8/es_ES.UTF-8/de_DE.UTF-8/ru_RU.UTF-8/' + . 'pt_BR.UTF-8/it_IT.UTF-8/ja_JP.UTF-8/zh_CN.UTF-8')), + 'hint' => $l->t('Please install one of these locales on your system and restart your webserver.') + ); + } + + // Contains the dependencies that should be checked against + // classes = class_exists + // functions = function_exists + // defined = defined + // ini = ini_get + // If the dependency is not found the missing module name is shown to the EndUser + // When adding new checks always verify that they pass on Travis as well + // for ini settings, see https://github.com/owncloud/administration/blob/master/travis-ci/custom.ini + $dependencies = array( + 'classes' => array( + 'ZipArchive' => 'zip', + 'DOMDocument' => 'dom', + 'XMLWriter' => 'XMLWriter', + 'XMLReader' => 'XMLReader', + ), + 'functions' => [ + 'xml_parser_create' => 'libxml', + 'mb_detect_encoding' => 'mb multibyte', + 'ctype_digit' => 'ctype', + 'json_encode' => 'JSON', + 'gd_info' => 'GD', + 'gzencode' => 'zlib', + 'iconv' => 'iconv', + 'simplexml_load_string' => 'SimpleXML', + 'hash' => 'HASH Message Digest Framework', + 'curl_init' => 'cURL', + ], + 'defined' => array( + 'PDO::ATTR_DRIVER_NAME' => 'PDO' + ), + 'ini' => [ + 'default_charset' => 'UTF-8', + ], + ); + $missingDependencies = array(); + $invalidIniSettings = []; + $moduleHint = $l->t('Please ask your server administrator to install the module.'); + + /** + * FIXME: The dependency check does not work properly on HHVM on the moment + * and prevents installation. Once HHVM is more compatible with our + * approach to check for these values we should re-enable those + * checks. + */ + $iniWrapper = \OC::$server->getIniWrapper(); + if (!self::runningOnHhvm()) { + foreach ($dependencies['classes'] as $class => $module) { + if (!class_exists($class)) { + $missingDependencies[] = $module; + } + } + foreach ($dependencies['functions'] as $function => $module) { + if (!function_exists($function)) { + $missingDependencies[] = $module; + } + } + foreach ($dependencies['defined'] as $defined => $module) { + if (!defined($defined)) { + $missingDependencies[] = $module; + } + } + foreach ($dependencies['ini'] as $setting => $expected) { + if (is_bool($expected)) { + if ($iniWrapper->getBool($setting) !== $expected) { + $invalidIniSettings[] = [$setting, $expected]; + } + } + if (is_int($expected)) { + if ($iniWrapper->getNumeric($setting) !== $expected) { + $invalidIniSettings[] = [$setting, $expected]; + } + } + if (is_string($expected)) { + if (strtolower($iniWrapper->getString($setting)) !== strtolower($expected)) { + $invalidIniSettings[] = [$setting, $expected]; + } + } + } + } + + foreach($missingDependencies as $missingDependency) { + $errors[] = array( + 'error' => $l->t('PHP module %s not installed.', array($missingDependency)), + 'hint' => $moduleHint + ); + $webServerRestart = true; + } + foreach($invalidIniSettings as $setting) { + if(is_bool($setting[1])) { + $setting[1] = ($setting[1]) ? 'on' : 'off'; + } + $errors[] = [ + 'error' => $l->t('PHP setting "%s" is not set to "%s".', [$setting[0], var_export($setting[1], true)]), + 'hint' => $l->t('Adjusting this setting in php.ini will make ownCloud run again') + ]; + $webServerRestart = true; + } + + /** + * The mbstring.func_overload check can only be performed if the mbstring + * module is installed as it will return null if the checking setting is + * not available and thus a check on the boolean value fails. + * + * TODO: Should probably be implemented in the above generic dependency + * check somehow in the long-term. + */ + if($iniWrapper->getBool('mbstring.func_overload') !== null && + $iniWrapper->getBool('mbstring.func_overload') === true) { + $errors[] = array( + 'error' => $l->t('mbstring.func_overload is set to "%s" instead of the expected value "0"', [$iniWrapper->getString('mbstring.func_overload')]), + 'hint' => $l->t('To fix this issue set <code>mbstring.func_overload</code> to <code>0</code> in your php.ini') + ); + } + + if(function_exists('xml_parser_create') && + version_compare('2.7.0', LIBXML_DOTTED_VERSION) === 1) { + $errors[] = array( + 'error' => $l->t('libxml2 2.7.0 is at least required. Currently %s is installed.', [LIBXML_DOTTED_VERSION]), + 'hint' => $l->t('To fix this issue update your libxml2 version and restart your web server.') + ); + } + + if (!self::isAnnotationsWorking()) { + $errors[] = array( + 'error' => $l->t('PHP is apparently set up to strip inline doc blocks. This will make several core apps inaccessible.'), + 'hint' => $l->t('This is probably caused by a cache/accelerator such as Zend OPcache or eAccelerator.') + ); + } + + if (!\OC::$CLI && $webServerRestart) { + $errors[] = array( + 'error' => $l->t('PHP modules have been installed, but they are still listed as missing?'), + 'hint' => $l->t('Please ask your server administrator to restart the web server.') + ); + } + + $errors = array_merge($errors, self::checkDatabaseVersion()); + + // Cache the result of this function + \OC::$server->getSession()->set('checkServer_succeeded', count($errors) == 0); + + return $errors; + } + + /** + * Check the database version + * + * @return array errors array + */ + public static function checkDatabaseVersion() { + $l = \OC::$server->getL10N('lib'); + $errors = array(); + $dbType = \OC::$server->getSystemConfig()->getValue('dbtype', 'sqlite'); + if ($dbType === 'pgsql') { + // check PostgreSQL version + try { + $result = \OC_DB::executeAudited('SHOW SERVER_VERSION'); + $data = $result->fetchRow(); + if (isset($data['server_version'])) { + $version = $data['server_version']; + if (version_compare($version, '9.0.0', '<')) { + $errors[] = array( + 'error' => $l->t('PostgreSQL >= 9 required'), + 'hint' => $l->t('Please upgrade your database version') + ); + } + } + } catch (\Doctrine\DBAL\DBALException $e) { + $logger = \OC::$server->getLogger(); + $logger->warning('Error occurred while checking PostgreSQL version, assuming >= 9'); + $logger->logException($e); + } + } + return $errors; + } + + /** + * Check for correct file permissions of data directory + * + * @param string $dataDirectory + * @return array arrays with error messages and hints + */ + public static function checkDataDirectoryPermissions($dataDirectory) { + $l = \OC::$server->getL10N('lib'); + $errors = array(); + if (self::runningOnWindows()) { + //TODO: permissions checks for windows hosts + } else { + $permissionsModHint = $l->t('Please change the permissions to 0770 so that the directory' + . ' cannot be listed by other users.'); + $perms = substr(decoct(@fileperms($dataDirectory)), -3); + if (substr($perms, -1) != '0') { + chmod($dataDirectory, 0770); + clearstatcache(); + $perms = substr(decoct(@fileperms($dataDirectory)), -3); + if (substr($perms, 2, 1) != '0') { + $errors[] = array( + 'error' => $l->t('Data directory (%s) is readable by other users', array($dataDirectory)), + 'hint' => $permissionsModHint + ); + } + } + } + return $errors; + } + + /** + * Check that the data directory exists and is valid by + * checking the existence of the ".ocdata" file. + * + * @param string $dataDirectory data directory path + * @return array errors found + */ + public static function checkDataDirectoryValidity($dataDirectory) { + $l = \OC::$server->getL10N('lib'); + $errors = []; + if (!self::runningOnWindows() && $dataDirectory[0] !== '/') { + $errors[] = [ + 'error' => $l->t('Data directory (%s) must be an absolute path', [$dataDirectory]), + 'hint' => $l->t('Check the value of "datadirectory" in your configuration') + ]; + } + if (!file_exists($dataDirectory . '/.ocdata')) { + $errors[] = [ + 'error' => $l->t('Data directory (%s) is invalid', [$dataDirectory]), + 'hint' => $l->t('Please check that the data directory contains a file' . + ' ".ocdata" in its root.') + ]; + } + return $errors; + } + + /** + * Check if the user is logged in, redirects to home if not. With + * redirect URL parameter to the request URI. + * + * @return void + */ + public static function checkLoggedIn() { + // Check if we are a user + if (!OC_User::isLoggedIn()) { + header('Location: ' . \OC::$server->getURLGenerator()->linkToRoute( + 'core.login.showLoginForm', + [ + 'redirect_url' => \OC::$server->getRequest()->getRequestUri() + ] + ) + ); + exit(); + } + } + + /** + * Check if the user is a admin, redirects to home if not + * + * @return void + */ + public static function checkAdminUser() { + OC_Util::checkLoggedIn(); + if (!OC_User::isAdminUser(OC_User::getUser())) { + header('Location: ' . \OCP\Util::linkToAbsolute('', 'index.php')); + exit(); + } + } + + /** + * Check if it is allowed to remember login. + * + * @note Every app can set 'rememberlogin' to 'false' to disable the remember login feature + * + * @return bool + */ + public static function rememberLoginAllowed() { + + $apps = OC_App::getEnabledApps(); + + foreach ($apps as $app) { + $appInfo = OC_App::getAppInfo($app); + if (isset($appInfo['rememberlogin']) && $appInfo['rememberlogin'] === 'false') { + return false; + } + + } + return true; + } + + /** + * Check if the user is a subadmin, redirects to home if not + * + * @return null|boolean $groups where the current user is subadmin + */ + public static function checkSubAdminUser() { + OC_Util::checkLoggedIn(); + $userObject = \OC::$server->getUserSession()->getUser(); + $isSubAdmin = false; + if($userObject !== null) { + $isSubAdmin = \OC::$server->getGroupManager()->getSubAdmin()->isSubAdmin($userObject); + } + + if (!$isSubAdmin) { + header('Location: ' . \OCP\Util::linkToAbsolute('', 'index.php')); + exit(); + } + return true; + } + + /** + * Returns the URL of the default page + * based on the system configuration and + * the apps visible for the current user + * + * @return string URL + */ + public static function getDefaultPageUrl() { + $urlGenerator = \OC::$server->getURLGenerator(); + // Deny the redirect if the URL contains a @ + // This prevents unvalidated redirects like ?redirect_url=:user@domain.com + if (isset($_REQUEST['redirect_url']) && strpos($_REQUEST['redirect_url'], '@') === false) { + $location = $urlGenerator->getAbsoluteURL(urldecode($_REQUEST['redirect_url'])); + } else { + $defaultPage = \OC::$server->getAppConfig()->getValue('core', 'defaultpage'); + if ($defaultPage) { + $location = $urlGenerator->getAbsoluteURL($defaultPage); + } else { + $appId = 'files'; + $defaultApps = explode(',', \OCP\Config::getSystemValue('defaultapp', 'files')); + // find the first app that is enabled for the current user + foreach ($defaultApps as $defaultApp) { + $defaultApp = OC_App::cleanAppId(strip_tags($defaultApp)); + if (static::getAppManager()->isEnabledForUser($defaultApp)) { + $appId = $defaultApp; + break; + } + } + + if(getenv('front_controller_active') === 'true') { + $location = $urlGenerator->getAbsoluteURL('/apps/' . $appId . '/'); + } else { + $location = $urlGenerator->getAbsoluteURL('/index.php/apps/' . $appId . '/'); + } + } + } + return $location; + } + + /** + * Redirect to the user default page + * + * @return void + */ + public static function redirectToDefaultPage() { + $location = self::getDefaultPageUrl(); + header('Location: ' . $location); + exit(); + } + + /** + * get an id unique for this instance + * + * @return string + */ + public static function getInstanceId() { + $id = \OC::$server->getSystemConfig()->getValue('instanceid', null); + if (is_null($id)) { + // We need to guarantee at least one letter in instanceid so it can be used as the session_name + $id = 'oc' . \OC::$server->getSecureRandom()->generate(10, \OCP\Security\ISecureRandom::CHAR_LOWER.\OCP\Security\ISecureRandom::CHAR_DIGITS); + \OC::$server->getSystemConfig()->setValue('instanceid', $id); + } + return $id; + } + + /** + * Public function to sanitize HTML + * + * This function is used to sanitize HTML and should be applied on any + * string or array of strings before displaying it on a web page. + * + * @param string|array $value + * @return string|array an array of sanitized strings or a single sanitized string, depends on the input parameter. + */ + public static function sanitizeHTML($value) { + if (is_array($value)) { + $value = array_map(function($value) { + return self::sanitizeHTML($value); + }, $value); + } else { + // Specify encoding for PHP<5.4 + $value = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); + } + return $value; + } + + /** + * Public function to encode url parameters + * + * This function is used to encode path to file before output. + * Encoding is done according to RFC 3986 with one exception: + * Character '/' is preserved as is. + * + * @param string $component part of URI to encode + * @return string + */ + public static function encodePath($component) { + $encoded = rawurlencode($component); + $encoded = str_replace('%2F', '/', $encoded); + return $encoded; + } + + /** + * Check if the .htaccess file is working + * @param \OCP\IConfig $config + * @return bool + * @throws Exception + * @throws \OC\HintException If the test file can't get written. + */ + public function isHtaccessWorking(\OCP\IConfig $config) { + + if (\OC::$CLI || !$config->getSystemValue('check_for_working_htaccess', true)) { + return true; + } + + // php dev server does not support htaccess + if (php_sapi_name() === 'cli-server') { + return false; + } + + // testdata + $fileName = '/htaccesstest.txt'; + $testContent = 'testcontent'; + + // creating a test file + $testFile = $config->getSystemValue('datadirectory', OC::$SERVERROOT . '/data') . '/' . $fileName; + + if (file_exists($testFile)) {// already running this test, possible recursive call + return false; + } + + $fp = @fopen($testFile, 'w'); + if (!$fp) { + throw new OC\HintException('Can\'t create test file to check for working .htaccess file.', + 'Make sure it is possible for the webserver to write to ' . $testFile); + } + fwrite($fp, $testContent); + fclose($fp); + + // accessing the file via http + $url = \OC::$server->getURLGenerator()->getAbsoluteURL(OC::$WEBROOT . '/data' . $fileName); + try { + $content = \OC::$server->getHTTPClientService()->newClient()->get($url)->getBody(); + } catch (\Exception $e) { + $content = false; + } + + // cleanup + @unlink($testFile); + + /* + * If the content is not equal to test content our .htaccess + * is working as required + */ + return $content !== $testContent; + } + + /** + * Check if the setlocal call does not work. This can happen if the right + * local packages are not available on the server. + * + * @return bool + */ + public static function isSetLocaleWorking() { + // setlocale test is pointless on Windows + if (OC_Util::runningOnWindows()) { + return true; + } + + \Patchwork\Utf8\Bootup::initLocale(); + if ('' === basename('ยง')) { + return false; + } + return true; + } + + /** + * Check if it's possible to get the inline annotations + * + * @return bool + */ + public static function isAnnotationsWorking() { + $reflection = new \ReflectionMethod(__METHOD__); + $docs = $reflection->getDocComment(); + + return (is_string($docs) && strlen($docs) > 50); + } + + /** + * Check if the PHP module fileinfo is loaded. + * + * @return bool + */ + public static function fileInfoLoaded() { + return function_exists('finfo_open'); + } + + /** + * clear all levels of output buffering + * + * @return void + */ + public static function obEnd() { + while (ob_get_level()) { + ob_end_clean(); + } + } + + /** + * Checks whether the server is running on Windows + * + * @return bool true if running on Windows, false otherwise + */ + public static function runningOnWindows() { + return (substr(PHP_OS, 0, 3) === "WIN"); + } + + /** + * Checks whether the server is running on Mac OS X + * + * @return bool true if running on Mac OS X, false otherwise + */ + public static function runningOnMac() { + return (strtoupper(substr(PHP_OS, 0, 6)) === 'DARWIN'); + } + + /** + * Checks whether server is running on HHVM + * + * @return bool True if running on HHVM, false otherwise + */ + public static function runningOnHhvm() { + return defined('HHVM_VERSION'); + } + + /** + * Handles the case that there may not be a theme, then check if a "default" + * theme exists and take that one + * + * @return string the theme + */ + public static function getTheme() { + $theme = \OC::$server->getSystemConfig()->getValue("theme", ''); + + if ($theme === '') { + if (is_dir(OC::$SERVERROOT . '/themes/default')) { + $theme = 'default'; + } + } + + return $theme; + } + + /** + * Clear a single file from the opcode cache + * This is useful for writing to the config file + * in case the opcode cache does not re-validate files + * Returns true if successful, false if unsuccessful: + * caller should fall back on clearing the entire cache + * with clearOpcodeCache() if unsuccessful + * + * @param string $path the path of the file to clear from the cache + * @return bool true if underlying function returns true, otherwise false + */ + public static function deleteFromOpcodeCache($path) { + $ret = false; + if ($path) { + // APC >= 3.1.1 + if (function_exists('apc_delete_file')) { + $ret = @apc_delete_file($path); + } + // Zend OpCache >= 7.0.0, PHP >= 5.5.0 + if (function_exists('opcache_invalidate')) { + $ret = opcache_invalidate($path); + } + } + return $ret; + } + + /** + * Clear the opcode cache if one exists + * This is necessary for writing to the config file + * in case the opcode cache does not re-validate files + * + * @return void + */ + public static function clearOpcodeCache() { + // APC + if (function_exists('apc_clear_cache')) { + apc_clear_cache(); + } + // Zend Opcache + if (function_exists('accelerator_reset')) { + accelerator_reset(); + } + // XCache + if (function_exists('xcache_clear_cache')) { + if (\OC::$server->getIniWrapper()->getBool('xcache.admin.enable_auth')) { + \OCP\Util::writeLog('core', 'XCache opcode cache will not be cleared because "xcache.admin.enable_auth" is enabled.', \OCP\Util::WARN); + } else { + @xcache_clear_cache(XC_TYPE_PHP, 0); + } + } + // Opcache (PHP >= 5.5) + if (function_exists('opcache_reset')) { + opcache_reset(); + } + } + + /** + * Normalize a unicode string + * + * @param string $value a not normalized string + * @return bool|string + */ + public static function normalizeUnicode($value) { + if(Normalizer::isNormalized($value)) { + return $value; + } + + $normalizedValue = Normalizer::normalize($value); + if ($normalizedValue === null || $normalizedValue === false) { + \OC::$server->getLogger()->warning('normalizing failed for "' . $value . '"', ['app' => 'core']); + return $value; + } + + return $normalizedValue; + } + + /** + * @param boolean|string $file + * @return string + */ + public static function basename($file) { + $file = rtrim($file, '/'); + $t = explode('/', $file); + return array_pop($t); + } + + /** + * A human readable string is generated based on version, channel and build number + * + * @return string + */ + public static function getHumanVersion() { + $version = OC_Util::getVersionString() . ' (' . OC_Util::getChannel() . ')'; + $build = OC_Util::getBuild(); + if (!empty($build) and OC_Util::getChannel() === 'daily') { + $version .= ' Build:' . $build; + } + return $version; + } + + /** + * Returns whether the given file name is valid + * + * @param string $file file name to check + * @return bool true if the file name is valid, false otherwise + * @deprecated use \OC\Files\View::verifyPath() + */ + public static function isValidFileName($file) { + $trimmed = trim($file); + if ($trimmed === '') { + return false; + } + if (\OC\Files\Filesystem::isIgnoredDir($trimmed)) { + return false; + } + foreach (str_split($trimmed) as $char) { + if (strpos(\OCP\Constants::FILENAME_INVALID_CHARS, $char) !== false) { + return false; + } + } + return true; + } + + /** + * Check whether the instance needs to perform an upgrade, + * either when the core version is higher or any app requires + * an upgrade. + * + * @param \OCP\IConfig $config + * @return bool whether the core or any app needs an upgrade + * @throws \OC\HintException When the upgrade from the given version is not allowed + */ + public static function needUpgrade(\OCP\IConfig $config) { + if ($config->getSystemValue('installed', false)) { + $installedVersion = $config->getSystemValue('version', '0.0.0'); + $currentVersion = implode('.', \OCP\Util::getVersion()); + $versionDiff = version_compare($currentVersion, $installedVersion); + if ($versionDiff > 0) { + return true; + } else if ($config->getSystemValue('debug', false) && $versionDiff < 0) { + // downgrade with debug + $installedMajor = explode('.', $installedVersion); + $installedMajor = $installedMajor[0] . '.' . $installedMajor[1]; + $currentMajor = explode('.', $currentVersion); + $currentMajor = $currentMajor[0] . '.' . $currentMajor[1]; + if ($installedMajor === $currentMajor) { + // Same major, allow downgrade for developers + return true; + } else { + // downgrade attempt, throw exception + throw new \OC\HintException('Downgrading is not supported and is likely to cause unpredictable issues (from ' . $installedVersion . ' to ' . $currentVersion . ')'); + } + } else if ($versionDiff < 0) { + // downgrade attempt, throw exception + throw new \OC\HintException('Downgrading is not supported and is likely to cause unpredictable issues (from ' . $installedVersion . ' to ' . $currentVersion . ')'); + } + + // also check for upgrades for apps (independently from the user) + $apps = \OC_App::getEnabledApps(false, true); + $shouldUpgrade = false; + foreach ($apps as $app) { + if (\OC_App::shouldUpgrade($app)) { + $shouldUpgrade = true; + break; + } + } + return $shouldUpgrade; + } else { + return false; + } + } + +} |