diff options
author | Sandro Lutz <sandro.lutz@temparus.ch> | 2017-02-07 00:12:45 +0100 |
---|---|---|
committer | Sandro Lutz <sandro.lutz@temparus.ch> | 2017-02-07 00:15:30 +0100 |
commit | fa1d607bfa951711a2c358f889db56962c179153 (patch) | |
tree | 904b6bd3b7f9d2ed133f64da22b3fb9bbfbf1842 /lib/private | |
parent | ff3fa538e43bb38a5ff142b07216b9de79645c01 (diff) | |
parent | b55f5af7eaab6f827989407fa7b8d51cbb877eab (diff) | |
download | nextcloud-server-fa1d607bfa951711a2c358f889db56962c179153.tar.gz nextcloud-server-fa1d607bfa951711a2c358f889db56962c179153.zip |
Merge remote-tracking branch 'nextcloud/master'
Signed-off-by: Sandro Lutz <sandro.lutz@temparus.ch>
Diffstat (limited to 'lib/private')
92 files changed, 1933 insertions, 3285 deletions
diff --git a/lib/private/AllConfig.php b/lib/private/AllConfig.php index 4e13d70371b..1f59e677a54 100644 --- a/lib/private/AllConfig.php +++ b/lib/private/AllConfig.php @@ -68,7 +68,7 @@ class AllConfig implements \OCP\IConfig { /** * @param SystemConfig $systemConfig */ - function __construct(SystemConfig $systemConfig) { + public function __construct(SystemConfig $systemConfig) { $this->userCache = new CappedMemoryCache(); $this->systemConfig = $systemConfig; } @@ -358,12 +358,17 @@ class AllConfig implements \OCP\IConfig { * ] */ private function getUserValues($userId) { - // TODO - FIXME - $this->fixDIInit(); - if (isset($this->userCache[$userId])) { return $this->userCache[$userId]; } + if ($userId === null || $userId === '') { + $this->userCache[$userId]=array(); + return $this->userCache[$userId]; + } + + // TODO - FIXME + $this->fixDIInit(); + $data = array(); $query = 'SELECT `appid`, `configkey`, `configvalue` FROM `*PREFIX*preferences` WHERE `userid` = ?'; $result = $this->connection->executeQuery($query, array($userId)); diff --git a/lib/private/App/AppManager.php b/lib/private/App/AppManager.php index fca5c9b87ac..6b819ef7ac1 100644 --- a/lib/private/App/AppManager.php +++ b/lib/private/App/AppManager.php @@ -221,6 +221,21 @@ class AppManager implements IAppManager { } /** + * Whether a list of types contains a protected app type + * + * @param string[] $types + * @return bool + */ + public function hasProtectedAppType($types) { + if (empty($types)) { + return false; + } + + $protectedTypes = array_intersect($this->protectedAppTypes, $types); + return !empty($protectedTypes); + } + + /** * Enable an app only for specific groups * * @param string $appId diff --git a/lib/private/App/AppStore/Fetcher/AppFetcher.php b/lib/private/App/AppStore/Fetcher/AppFetcher.php index bbe75c723d5..7c5efafc92f 100644 --- a/lib/private/App/AppStore/Fetcher/AppFetcher.php +++ b/lib/private/App/AppStore/Fetcher/AppFetcher.php @@ -28,9 +28,6 @@ use OCP\Http\Client\IClientService; use OCP\IConfig; class AppFetcher extends Fetcher { - /** @var IConfig */ - private $config; - /** * @param IAppData $appData * @param IClientService $clientService @@ -44,13 +41,13 @@ class AppFetcher extends Fetcher { parent::__construct( $appData, $clientService, - $timeFactory + $timeFactory, + $config ); $this->fileName = 'apps.json'; - $this->config = $config; - $versionArray = \OC_Util::getVersion(); + $versionArray = explode('.', $this->config->getSystemValue('version')); $this->endpointUrl = sprintf( 'https://apps.nextcloud.com/api/v1/platform/%d.%d.%d/apps.json', $versionArray[0], @@ -62,15 +59,14 @@ class AppFetcher extends Fetcher { /** * Only returns the latest compatible app release in the releases array * + * @param string $ETag + * @param string $content + * * @return array */ - protected function fetch() { - $client = $this->clientService->newClient(); - $response = $client->get($this->endpointUrl); - $responseJson = []; - $responseJson['data'] = json_decode($response->getBody(), true); - $responseJson['timestamp'] = $this->timeFactory->getTime(); - $response = $responseJson; + protected function fetch($ETag, $content) { + /** @var mixed[] $response */ + $response = parent::fetch($ETag, $content); $ncVersion = $this->config->getSystemValue('version'); $ncMajorVersion = explode('.', $ncVersion)[0]; diff --git a/lib/private/App/AppStore/Fetcher/CategoryFetcher.php b/lib/private/App/AppStore/Fetcher/CategoryFetcher.php index 74201ec3737..8b79259a66a 100644 --- a/lib/private/App/AppStore/Fetcher/CategoryFetcher.php +++ b/lib/private/App/AppStore/Fetcher/CategoryFetcher.php @@ -24,20 +24,24 @@ namespace OC\App\AppStore\Fetcher; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Files\IAppData; use OCP\Http\Client\IClientService; +use OCP\IConfig; class CategoryFetcher extends Fetcher { /** * @param IAppData $appData * @param IClientService $clientService * @param ITimeFactory $timeFactory + * @param IConfig $config */ public function __construct(IAppData $appData, IClientService $clientService, - ITimeFactory $timeFactory) { + ITimeFactory $timeFactory, + IConfig $config) { parent::__construct( $appData, $clientService, - $timeFactory + $timeFactory, + $config ); $this->fileName = 'categories.json'; $this->endpointUrl = 'https://apps.nextcloud.com/api/v1/categories.json'; diff --git a/lib/private/App/AppStore/Fetcher/Fetcher.php b/lib/private/App/AppStore/Fetcher/Fetcher.php index f82e1a253f6..dab79e11821 100644 --- a/lib/private/App/AppStore/Fetcher/Fetcher.php +++ b/lib/private/App/AppStore/Fetcher/Fetcher.php @@ -21,10 +21,12 @@ namespace OC\App\AppStore\Fetcher; +use OCP\AppFramework\Http; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Files\IAppData; use OCP\Files\NotFoundException; use OCP\Http\Client\IClientService; +use OCP\IConfig; abstract class Fetcher { const INVALIDATE_AFTER_SECONDS = 300; @@ -35,6 +37,8 @@ abstract class Fetcher { protected $clientService; /** @var ITimeFactory */ protected $timeFactory; + /** @var IConfig */ + protected $config; /** @var string */ protected $fileName; /** @var string */ @@ -44,26 +48,52 @@ abstract class Fetcher { * @param IAppData $appData * @param IClientService $clientService * @param ITimeFactory $timeFactory + * @param IConfig $config */ public function __construct(IAppData $appData, IClientService $clientService, - ITimeFactory $timeFactory) { + ITimeFactory $timeFactory, + IConfig $config) { $this->appData = $appData; $this->clientService = $clientService; $this->timeFactory = $timeFactory; + $this->config = $config; } /** * Fetches the response from the server * + * @param string $ETag + * @param string $content + * * @return array */ - protected function fetch() { + protected function fetch($ETag, $content) { + $options = []; + + if ($ETag !== '') { + $options['headers'] = [ + 'If-None-Match' => $ETag, + ]; + } + $client = $this->clientService->newClient(); - $response = $client->get($this->endpointUrl); + $response = $client->get($this->endpointUrl, $options); + $responseJson = []; - $responseJson['data'] = json_decode($response->getBody(), true); + if ($response->getStatusCode() === Http::STATUS_NOT_MODIFIED) { + $responseJson['data'] = json_decode($content, true); + } else { + $responseJson['data'] = json_decode($response->getBody(), true); + $ETag = $response->getHeader('ETag'); + } + $responseJson['timestamp'] = $this->timeFactory->getTime(); + $responseJson['ncversion'] = $this->config->getSystemValue('version'); + if ($ETag !== '') { + $responseJson['ETag'] = $ETag; + } + return $responseJson; } @@ -75,15 +105,27 @@ abstract class Fetcher { public function get() { $rootFolder = $this->appData->getFolder('/'); + $ETag = ''; + $content = ''; + try { // File does already exists $file = $rootFolder->getFile($this->fileName); $jsonBlob = json_decode($file->getContent(), true); if(is_array($jsonBlob)) { - // If the timestamp is older than 300 seconds request the files new - if((int)$jsonBlob['timestamp'] > ($this->timeFactory->getTime() - self::INVALIDATE_AFTER_SECONDS)) { + /* + * If the timestamp is older than 300 seconds request the files new + * If the version changed (update!) also refresh + */ + if((int)$jsonBlob['timestamp'] > ($this->timeFactory->getTime() - self::INVALIDATE_AFTER_SECONDS) && + isset($jsonBlob['ncversion']) && $jsonBlob['ncversion'] === $this->config->getSystemValue('version', '0.0.0')) { return $jsonBlob['data']; } + + if (isset($jsonBlob['ETag'])) { + $ETag = $jsonBlob['ETag']; + $content = json_encode($jsonBlob['data']); + } } } catch (NotFoundException $e) { // File does not already exists @@ -92,7 +134,7 @@ abstract class Fetcher { // Refresh the file content try { - $responseJson = $this->fetch(); + $responseJson = $this->fetch($ETag, $content); $file->putContent(json_encode($responseJson)); return json_decode($file->getContent(), true)['data']; } catch (\Exception $e) { diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index d92e8965b5c..4e102522550 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -1,5 +1,6 @@ <?php /** + * @copyright Copyright (c) 2017, Joas Schilling <coding@schilljs.com> * @copyright Copyright (c) 2016, ownCloud, Inc. * * @author Arthur Schiwon <blizzz@arthur-schiwon.de> @@ -29,7 +30,9 @@ namespace OC; +use OC\DB\OracleConnection; use OCP\IAppConfig; +use OCP\IConfig; use OCP\IDBConnection; /** @@ -37,12 +40,25 @@ use OCP\IDBConnection; * database. */ class AppConfig implements IAppConfig { - /** - * @var \OCP\IDBConnection $conn - */ + + /** @var array[] */ + protected $sensitiveValues = [ + 'spreed' => [ + 'turn_server_secret', + ], + 'user_ldap' => [ + 'ldap_agent_password', + ], + ]; + + /** @var \OCP\IDBConnection */ protected $conn; - private $cache = array(); + /** @var array[] */ + private $cache = []; + + /** @var bool */ + private $configLoaded = false; /** * @param IDBConnection $conn @@ -85,6 +101,7 @@ class AppConfig implements IAppConfig { * * @param string $app the app we are looking for * @return array an array of key names + * @deprecated 8.0.0 use method getAppKeys of \OCP\IConfig * * This function gets all keys of an app. Please note that the values are * not returned. @@ -112,6 +129,7 @@ class AppConfig implements IAppConfig { * @param string $key key * @param string $default = null, default value if the key does not exist * @return string the value or $default + * @deprecated 8.0.0 use method getAppValue of \OCP\IConfig * * This function gets a value from the appconfig table. If the key does * not exist the default value will be returned @@ -146,6 +164,7 @@ class AppConfig implements IAppConfig { * @param string $key key * @param string|float|int $value value * @return bool True if the value was inserted or updated, false if the value was the same + * @deprecated 8.0.0 use method setAppValue of \OCP\IConfig */ public function setValue($app, $key, $value) { if (!$this->hasKey($app, $key)) { @@ -182,7 +201,7 @@ class AppConfig implements IAppConfig { * http://docs.oracle.com/cd/E11882_01/server.112/e26088/conditions002.htm#i1033286 * > Large objects (LOBs) are not supported in comparison conditions. */ - if (!($this->conn instanceof \OC\DB\OracleConnection)) { + if (!($this->conn instanceof OracleConnection)) { // Only update the value when it is not the same $sql->andWhere($sql->expr()->neq('configvalue', $sql->createParameter('configvalue'))) ->setParameter('configvalue', $value); @@ -200,7 +219,8 @@ class AppConfig implements IAppConfig { * * @param string $app app * @param string $key key - * @return boolean|null + * @return boolean + * @deprecated 8.0.0 use method deleteAppValue of \OCP\IConfig */ public function deleteKey($app, $key) { $this->loadConfigValues(); @@ -214,13 +234,15 @@ class AppConfig implements IAppConfig { $sql->execute(); unset($this->cache[$app][$key]); + return false; } /** * Remove app from appconfig * * @param string $app app - * @return boolean|null + * @return boolean + * @deprecated 8.0.0 use method deleteAppValue of \OCP\IConfig * * Removes all keys in appconfig belonging to the app. */ @@ -234,6 +256,7 @@ class AppConfig implements IAppConfig { $sql->execute(); unset($this->cache[$app]); + return false; } /** @@ -262,10 +285,30 @@ class AppConfig implements IAppConfig { } /** + * get all values of the app or and filters out sensitive data + * + * @param string $app + * @return array + */ + public function getFilteredValues($app) { + $values = $this->getValues($app, false); + + foreach ($this->sensitiveValues[$app] as $sensitiveKey) { + if (isset($values[$sensitiveKey])) { + $values[$sensitiveKey] = IConfig::SENSITIVE_VALUE; + } + } + + return $values; + } + + /** * Load all the app config values */ protected function loadConfigValues() { - if ($this->configLoaded) return; + if ($this->configLoaded) { + return; + } $this->cache = []; diff --git a/lib/private/AppFramework/Db/Db.php b/lib/private/AppFramework/Db/Db.php deleted file mode 100644 index 5aacdc517a2..00000000000 --- a/lib/private/AppFramework/Db/Db.php +++ /dev/null @@ -1,314 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bernhard Posselt <dev@bernhard-posselt.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\AppFramework\Db; - -use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\IDb; -use OCP\IDBConnection; -use OCP\PreConditionNotMetException; -use Doctrine\DBAL\Platforms\MySqlPlatform; - -/** - * @deprecated use IDBConnection directly, will be removed in ownCloud 10 - * Small Facade for being able to inject the database connection for tests - */ -class Db implements IDb { - /** - * @var IDBConnection - */ - protected $connection; - - /** - * @param IDBConnection $connection - */ - public function __construct(IDBConnection $connection) { - $this->connection = $connection; - } - - /** - * Gets the ExpressionBuilder for the connection. - * - * @return \OCP\DB\QueryBuilder\IQueryBuilder - */ - public function getQueryBuilder() { - return $this->connection->getQueryBuilder(); - } - - /** - * Used to abstract the ownCloud database access away - * - * @param string $sql the sql query with ? placeholder for params - * @param int $limit the maximum number of rows - * @param int $offset from which row we want to start - * @deprecated use prepare instead, will be removed in ownCloud 10 - * @return \OC_DB_StatementWrapper prepared SQL query - */ - public function prepareQuery($sql, $limit = null, $offset = null) { - $isManipulation = \OC_DB::isManipulation($sql); - $statement = $this->connection->prepare($sql, $limit, $offset); - return new \OC_DB_StatementWrapper($statement, $isManipulation); - } - - - /** - * Used to get the id of the just inserted element - * - * @deprecated use lastInsertId instead, will be removed in ownCloud 10 - * @param string $tableName the name of the table where we inserted the item - * @return int the id of the inserted element - */ - public function getInsertId($tableName) { - return $this->connection->lastInsertId($tableName); - } - - /** - * Used to abstract the ownCloud database access away - * @param string $sql the sql query with ? placeholder for params - * @param int $limit the maximum number of rows - * @param int $offset from which row we want to start - * @return \Doctrine\DBAL\Driver\Statement The prepared statement. - */ - public function prepare($sql, $limit=null, $offset=null) { - return $this->connection->prepare($sql, $limit, $offset); - } - - /** - * Executes an, optionally parameterized, SQL query. - * - * If the query is parameterized, a prepared statement is used. - * If an SQLLogger is configured, the execution is logged. - * - * @param string $query The SQL query to execute. - * @param string[] $params The parameters to bind to the query, if any. - * @param array $types The types the previous parameters are in. - * @return \Doctrine\DBAL\Driver\Statement The executed statement. - */ - public function executeQuery($query, array $params = array(), $types = array()) { - return $this->connection->executeQuery($query, $params, $types); - } - - /** - * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters - * and returns the number of affected rows. - * - * This method supports PDO binding types as well as DBAL mapping types. - * - * @param string $query The SQL query. - * @param array $params The query parameters. - * @param array $types The parameter types. - * @return integer The number of affected rows. - */ - public function executeUpdate($query, array $params = array(), array $types = array()) { - return $this->connection->executeUpdate($query, $params, $types); - } - - /** - * Used to get the id of the just inserted element - * @param string $table the name of the table where we inserted the item - * @return int the id of the inserted element - */ - public function lastInsertId($table = null) { - return $this->connection->lastInsertId($table); - } - - /** - * Insert a row if the matching row does not exists. - * - * @param string $table The table name (will replace *PREFIX* with the actual prefix) - * @param array $input data that should be inserted into the table (column name => value) - * @param array|null $compare List of values that should be checked for "if not exists" - * If this is null or an empty array, all keys of $input will be compared - * Please note: text fields (clob) must not be used in the compare array - * @return int number of inserted rows - * @throws \Doctrine\DBAL\DBALException - */ - public function insertIfNotExist($table, $input, array $compare = null) { - return $this->connection->insertIfNotExist($table, $input, $compare); - } - - /** - * Insert or update a row value - * - * @param string $table - * @param array $keys (column name => value) - * @param array $values (column name => value) - * @param array $updatePreconditionValues ensure values match preconditions (column name => value) - * @return int number of new rows - * @throws \Doctrine\DBAL\DBALException - * @throws PreConditionNotMetException - */ - public function setValues($table, array $keys, array $values, array $updatePreconditionValues = []) { - return $this->connection->setValues($table, $keys, $values, $updatePreconditionValues); - } - - /** - * @inheritdoc - */ - public function lockTable($tableName) { - $this->connection->lockTable($tableName); - } - - /** - * @inheritdoc - */ - public function unlockTable() { - $this->connection->unlockTable(); - } - - /** - * Start a transaction - */ - public function beginTransaction() { - $this->connection->beginTransaction(); - } - - /** - * Check if a transaction is active - * - * @return bool - */ - public function inTransaction() { - return $this->connection->inTransaction(); - } - - /** - * Commit the database changes done during a transaction that is in progress - */ - public function commit() { - $this->connection->commit(); - } - - /** - * Rollback the database changes done during a transaction that is in progress - */ - public function rollBack() { - $this->connection->rollBack(); - } - - /** - * Gets the error code and message as a string for logging - * @return string - */ - public function getError() { - return $this->connection->getError(); - } - - /** - * Fetch the SQLSTATE associated with the last database operation. - * - * @return integer The last error code. - */ - public function errorCode() { - return $this->connection->errorCode(); - } - - /** - * Fetch extended error information associated with the last database operation. - * - * @return array The last error information. - */ - public function errorInfo() { - return $this->connection->errorInfo(); - } - - /** - * Establishes the connection with the database. - * - * @return bool - */ - public function connect() { - return $this->connection->connect(); - } - - /** - * Close the database connection - */ - public function close() { - $this->connection->close(); - } - - /** - * Quotes a given input parameter. - * - * @param mixed $input Parameter to be quoted. - * @param int $type Type of the parameter. - * @return string The quoted parameter. - */ - public function quote($input, $type = IQueryBuilder::PARAM_STR) { - return $this->connection->quote($input, $type); - } - - /** - * Gets the DatabasePlatform instance that provides all the metadata about - * the platform this driver connects to. - * - * @return \Doctrine\DBAL\Platforms\AbstractPlatform The database platform. - */ - public function getDatabasePlatform() { - return $this->connection->getDatabasePlatform(); - } - - /** - * Drop a table from the database if it exists - * - * @param string $table table name without the prefix - */ - public function dropTable($table) { - $this->connection->dropTable($table); - } - - /** - * Check if a table exists - * - * @param string $table table name without the prefix - * @return bool - */ - public function tableExists($table) { - return $this->connection->tableExists($table); - } - - /** - * Espace a parameter to be used in a LIKE query - * - * @param string $param - * @return string - */ - public function escapeLikeParameter($param) { - return $this->connection->escapeLikeParameter($param); - } - - /** - * Check whether or not the current database support 4byte wide unicode - * - * @return bool - * @since 11.0.0 - */ - public function supports4ByteText() { - return $this->connection->supports4ByteText(); - } -} diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index 148a15172ac..0879b3e9330 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -43,8 +43,10 @@ use OC\AppFramework\Middleware\OCSMiddleware; use OC\AppFramework\Middleware\Security\SecurityMiddleware; use OC\AppFramework\Middleware\SessionMiddleware; use OC\AppFramework\Utility\SimpleContainer; +use OC\AppFramework\Utility\TimeFactory; use OC\Core\Middleware\TwoFactorMiddleware; use OC\RichObjectStrings\Validator; +use OC\Security\Bruteforce\Throttler; use OCP\AppFramework\IApi; use OCP\AppFramework\IAppContainer; use OCP\Files\IAppData; @@ -91,6 +93,10 @@ class DIContainer extends SimpleContainer implements IAppContainer { return new Output($this->getServer()->getWebRoot()); }); + $this->registerService(\OCP\Authentication\LoginCredentials\IStore::class, function() { + return $this->getServer()->query(\OCP\Authentication\LoginCredentials\IStore::class); + }); + $this->registerService('OCP\\IAvatarManager', function($c) { return $this->getServer()->getAvatarManager(); }); @@ -134,10 +140,6 @@ class DIContainer extends SimpleContainer implements IAppContainer { return $this->getServer()->getDateTimeFormatter(); }); - $this->registerService('OCP\\IDb', function($c) { - return $this->getServer()->getDb(); - }); - $this->registerService('OCP\\IDBConnection', function($c) { return $this->getServer()->getDatabaseConnection(); }); @@ -380,20 +382,25 @@ class DIContainer extends SimpleContainer implements IAppContainer { */ $app = $this; $this->registerService('SecurityMiddleware', function($c) use ($app){ + /** @var \OC\Server $server */ + $server = $app->getServer(); + return new SecurityMiddleware( $c['Request'], $c['ControllerMethodReflector'], - $app->getServer()->getNavigationManager(), - $app->getServer()->getURLGenerator(), - $app->getServer()->getLogger(), - $app->getServer()->getSession(), + $server->getNavigationManager(), + $server->getURLGenerator(), + $server->getLogger(), + $server->getSession(), $c['AppName'], $app->isLoggedIn(), $app->isAdminUser(), - $app->getServer()->getContentSecurityPolicyManager(), - $app->getServer()->getCsrfTokenManager(), - $app->getServer()->getContentSecurityPolicyNonceManager() + $server->getContentSecurityPolicyManager(), + $server->getCsrfTokenManager(), + $server->getContentSecurityPolicyNonceManager(), + $server->getBruteForceThrottler() ); + }); $this->registerService('CORSMiddleware', function($c) { diff --git a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php index d60d5749d57..edba6a3e759 100644 --- a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php @@ -36,6 +36,7 @@ use OC\AppFramework\Middleware\Security\Exceptions\NotConfirmedException; use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException; use OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException; use OC\AppFramework\Utility\ControllerMethodReflector; +use OC\Security\Bruteforce\Throttler; use OC\Security\CSP\ContentSecurityPolicyManager; use OC\Security\CSP\ContentSecurityPolicyNonceManager; use OC\Security\CSRF\CsrfTokenManager; @@ -87,6 +88,8 @@ class SecurityMiddleware extends Middleware { private $csrfTokenManager; /** @var ContentSecurityPolicyNonceManager */ private $cspNonceManager; + /** @var Throttler */ + private $throttler; /** * @param IRequest $request @@ -101,6 +104,7 @@ class SecurityMiddleware extends Middleware { * @param ContentSecurityPolicyManager $contentSecurityPolicyManager * @param CSRFTokenManager $csrfTokenManager * @param ContentSecurityPolicyNonceManager $cspNonceManager + * @param Throttler $throttler */ public function __construct(IRequest $request, ControllerMethodReflector $reflector, @@ -113,7 +117,8 @@ class SecurityMiddleware extends Middleware { $isAdminUser, ContentSecurityPolicyManager $contentSecurityPolicyManager, CsrfTokenManager $csrfTokenManager, - ContentSecurityPolicyNonceManager $cspNonceManager) { + ContentSecurityPolicyNonceManager $cspNonceManager, + Throttler $throttler) { $this->navigationManager = $navigationManager; $this->request = $request; $this->reflector = $reflector; @@ -126,6 +131,7 @@ class SecurityMiddleware extends Middleware { $this->contentSecurityPolicyManager = $contentSecurityPolicyManager; $this->csrfTokenManager = $csrfTokenManager; $this->cspNonceManager = $cspNonceManager; + $this->throttler = $throttler; } @@ -185,6 +191,12 @@ class SecurityMiddleware extends Middleware { } } + if($this->reflector->hasAnnotation('BruteForceProtection')) { + $action = $this->reflector->getAnnotationParameter('BruteForceProtection'); + $this->throttler->sleepDelay($this->request->getRemoteAddress(), $action); + $this->throttler->registerAttempt($action, $this->request->getRemoteAddress()); + } + /** * FIXME: Use DI once available * Checks if app is enabled (also includes a check whether user is allowed to access the resource) diff --git a/lib/private/AppFramework/Utility/ControllerMethodReflector.php b/lib/private/AppFramework/Utility/ControllerMethodReflector.php index 33a117d8121..034fc3a1759 100644 --- a/lib/private/AppFramework/Utility/ControllerMethodReflector.php +++ b/lib/private/AppFramework/Utility/ControllerMethodReflector.php @@ -55,8 +55,10 @@ class ControllerMethodReflector implements IControllerMethodReflector{ $docs = $reflection->getDocComment(); // extract everything prefixed by @ and first letter uppercase - preg_match_all('/@([A-Z]\w+)/', $docs, $matches); - $this->annotations = $matches[1]; + preg_match_all('/^\h+\*\h+@(?P<annotation>[A-Z]\w+)(\h+(?P<parameter>\w+))?$/m', $docs, $matches); + foreach($matches['annotation'] as $key => $annontation) { + $this->annotations[$annontation] = $matches['parameter'][$key]; + } // extract type parameter information preg_match_all('/@param\h+(?P<type>\w+)\h+\$(?P<var>\w+)/', $docs, $matches); @@ -112,7 +114,22 @@ class ControllerMethodReflector implements IControllerMethodReflector{ * @return bool true if the annotation is found */ public function hasAnnotation($name){ - return in_array($name, $this->annotations); + return array_key_exists($name, $this->annotations); + } + + + /** + * Get optional annotation parameter + * @param string $name the name of the annotation + * @return string + */ + public function getAnnotationParameter($name){ + $parameter = ''; + if($this->hasAnnotation($name)) { + $parameter = $this->annotations[$name]; + } + + return $parameter; } diff --git a/lib/private/Archive/TAR.php b/lib/private/Archive/TAR.php index bbd24bd05a1..07ccd09f399 100644 --- a/lib/private/Archive/TAR.php +++ b/lib/private/Archive/TAR.php @@ -33,6 +33,8 @@ namespace OC\Archive; +use Icewind\Streams\CallbackWrapper; + class TAR extends Archive { const PLAIN = 0; const GZIP = 1; @@ -359,22 +361,19 @@ class TAR extends Archive { if ($mode == 'r' or $mode == 'rb') { return fopen($tmpFile, $mode); } else { - \OC\Files\Stream\Close::registerCallback($tmpFile, array($this, 'writeBack')); - self::$tempFiles[$tmpFile] = $path; - return fopen('close://' . $tmpFile, $mode); + $handle = fopen($tmpFile, $mode); + return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) { + $this->writeBack($tmpFile, $path); + }); } } - private static $tempFiles = array(); - /** * write back temporary files */ - function writeBack($tmpFile) { - if (isset(self::$tempFiles[$tmpFile])) { - $this->addFile(self::$tempFiles[$tmpFile], $tmpFile); - unlink($tmpFile); - } + function writeBack($tmpFile, $path) { + $this->addFile($path, $tmpFile); + unlink($tmpFile); } /** diff --git a/lib/private/Archive/ZIP.php b/lib/private/Archive/ZIP.php index 9e9fe40b2b4..0ed0f48acc4 100644 --- a/lib/private/Archive/ZIP.php +++ b/lib/private/Archive/ZIP.php @@ -31,6 +31,8 @@ namespace OC\Archive; +use Icewind\Streams\CallbackWrapper; + class ZIP extends Archive{ /** * @var \ZipArchive zip @@ -198,24 +200,22 @@ class ZIP extends Archive{ $ext=''; } $tmpFile=\OCP\Files::tmpFile($ext); - \OC\Files\Stream\Close::registerCallback($tmpFile, array($this, 'writeBack')); if($this->fileExists($path)) { $this->extractFile($path, $tmpFile); } - self::$tempFiles[$tmpFile]=$path; - return fopen('close://'.$tmpFile, $mode); + $handle = fopen($tmpFile, $mode); + return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) { + $this->writeBack($tmpFile, $path); + }); } } - private static $tempFiles=array(); /** * write back temporary files */ - function writeBack($tmpFile) { - if(isset(self::$tempFiles[$tmpFile])) { - $this->addFile(self::$tempFiles[$tmpFile], $tmpFile); - unlink($tmpFile); - } + function writeBack($tmpFile, $path) { + $this->addFile($path, $tmpFile); + unlink($tmpFile); } /** diff --git a/lib/private/Authentication/LoginCredentials/Credentials.php b/lib/private/Authentication/LoginCredentials/Credentials.php new file mode 100644 index 00000000000..9314b7489db --- /dev/null +++ b/lib/private/Authentication/LoginCredentials/Credentials.php @@ -0,0 +1,72 @@ +<?php + +/** + * @copyright 2016 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2016 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Authentication\LoginCredentials; + +use OCP\Authentication\LoginCredentials\ICredentials; + +class Credentials implements ICredentials { + + /** @var string */ + private $uid; + + /** @var string */ + private $loginName; + + /** @var string */ + private $password; + + /** + * @param string $uid + * @param string $loginName + * @param string $password + */ + public function __construct($uid, $loginName, $password) { + $this->uid = $uid; + $this->loginName = $loginName; + $this->password = $password; + } + + /** + * @return string + */ + public function getUID() { + return $this->uid; + } + + /** + * @return string + */ + public function getLoginName() { + return $this->loginName; + } + + /** + * @return string + */ + public function getPassword() { + return $this->password; + } + +} diff --git a/lib/private/Authentication/LoginCredentials/Store.php b/lib/private/Authentication/LoginCredentials/Store.php new file mode 100644 index 00000000000..e44c88c7aea --- /dev/null +++ b/lib/private/Authentication/LoginCredentials/Store.php @@ -0,0 +1,120 @@ +<?php + +/** + * @copyright 2016 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2016 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Authentication\LoginCredentials; + +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Exceptions\PasswordlessTokenException; +use OC\Authentication\Token\IProvider; +use OCP\Authentication\Exceptions\CredentialsUnavailableException; +use OCP\Authentication\LoginCredentials\ICredentials; +use OCP\Authentication\LoginCredentials\IStore; +use OCP\ILogger; +use OCP\ISession; +use OCP\Session\Exceptions\SessionNotAvailableException; +use OCP\Util; + +class Store implements IStore { + + /** @var ISession */ + private $session; + + /** @var ILogger */ + private $logger; + + /** @var IProvider|null */ + private $tokenProvider; + + /** + * @param ISession $session + * @param ILogger $logger + * @param IProvider $tokenProvider + */ + public function __construct(ISession $session, ILogger $logger, IProvider $tokenProvider = null) { + $this->session = $session; + $this->logger = $logger; + $this->tokenProvider = $tokenProvider; + + Util::connectHook('OC_User', 'post_login', $this, 'authenticate'); + } + + /** + * Hook listener on post login + * + * @param array $params + */ + public function authenticate(array $params) { + $this->session->set('login_credentials', json_encode($params)); + } + + /** + * Replace the session implementation + * + * @param ISession $session + */ + public function setSession(ISession $session) { + $this->session = $session; + } + + /** + * @since 12 + * + * @return ICredentials the login credentials of the current user + * @throws CredentialsUnavailableException + */ + public function getLoginCredentials() { + if (is_null($this->tokenProvider)) { + throw new CredentialsUnavailableException(); + } + + $trySession = false; + try { + $sessionId = $this->session->getId(); + $token = $this->tokenProvider->getToken($sessionId); + + $uid = $token->getUID(); + $user = $token->getLoginName(); + $password = $this->tokenProvider->getPassword($token, $sessionId); + + return new Credentials($uid, $user, $password); + } catch (SessionNotAvailableException $ex) { + $this->logger->debug('could not get login credentials because session is unavailable', ['app' => 'core']); + } catch (InvalidTokenException $ex) { + $this->logger->debug('could not get login credentials because the token is invalid', ['app' => 'core']); + $trySession = true; + } catch (PasswordlessTokenException $ex) { + $this->logger->debug('could not get login credentials because the token has no password', ['app' => 'core']); + $trySession = true; + } + + if ($trySession && $this->session->exists('login_credentials')) { + $creds = json_decode($this->session->get('login_credentials')); + return new Credentials($creds->uid, $creds->uid, $creds->password); + } + + // If we reach this line, an exception was thrown. + throw new CredentialsUnavailableException(); + } + +} diff --git a/lib/private/Authentication/TwoFactorAuth/Manager.php b/lib/private/Authentication/TwoFactorAuth/Manager.php index 48792aa685b..1d0deada696 100644 --- a/lib/private/Authentication/TwoFactorAuth/Manager.php +++ b/lib/private/Authentication/TwoFactorAuth/Manager.php @@ -1,4 +1,5 @@ <?php + /** * @copyright Copyright (c) 2016, ownCloud, Inc. * @@ -26,9 +27,11 @@ use Exception; use OC; use OC\App\AppManager; use OC_App; +use OCP\Activity\IManager; use OCP\AppFramework\QueryException; use OCP\Authentication\TwoFactorAuth\IProvider; use OCP\IConfig; +use OCP\ILogger; use OCP\ISession; use OCP\IUser; @@ -48,15 +51,26 @@ class Manager { /** @var IConfig */ private $config; + /** @var IManager */ + private $activityManager; + + /** @var ILogger */ + private $logger; + /** * @param AppManager $appManager * @param ISession $session * @param IConfig $config + * @param IManager $activityManager + * @param ILogger $logger */ - public function __construct(AppManager $appManager, ISession $session, IConfig $config) { + public function __construct(AppManager $appManager, ISession $session, IConfig $config, IManager $activityManager, + ILogger $logger) { $this->appManager = $appManager; $this->session = $session; $this->config = $config; + $this->activityManager = $activityManager; + $this->logger = $logger; } /** @@ -184,11 +198,40 @@ class Manager { } $this->session->remove(self::SESSION_UID_KEY); $this->session->remove(self::REMEMBER_LOGIN); + + $this->publishEvent($user, 'twofactor_success', [ + 'provider' => $provider->getDisplayName(), + ]); + } else { + $this->publishEvent($user, 'twofactor_failed', [ + 'provider' => $provider->getDisplayName(), + ]); } return $passed; } /** + * Push a 2fa event the user's activity stream + * + * @param IUser $user + * @param string $event + */ + private function publishEvent(IUser $user, $event, array $params) { + $activity = $this->activityManager->generateEvent(); + $activity->setApp('twofactor_generic') + ->setType('twofactor') + ->setAuthor($user->getUID()) + ->setAffectedUser($user->getUID()) + ->setSubject($event, $params); + try { + $this->activityManager->publish($activity); + } catch (Exception $e) { + $this->logger->warning('could not publish backup code creation activity', ['app' => 'twofactor_backupcodes']); + $this->logger->logException($e, ['app' => 'twofactor_backupcodes']); + } + } + + /** * Check if the currently logged in user needs to pass 2FA * * @param IUser $user the currently logged in user diff --git a/lib/private/Console/TimestampFormatter.php b/lib/private/Console/TimestampFormatter.php index 5b292439a4c..66dd38e6ac3 100644 --- a/lib/private/Console/TimestampFormatter.php +++ b/lib/private/Console/TimestampFormatter.php @@ -97,11 +97,11 @@ class TimestampFormatter implements OutputFormatterInterface { */ public function format($message) { - $timeZone = $this->config->getSystemValue('logtimezone', null); + $timeZone = $this->config->getSystemValue('logtimezone', 'UTC'); $timeZone = $timeZone !== null ? new \DateTimeZone($timeZone) : null; $time = new \DateTime('now', $timeZone); - $timestampInfo = $time->format($this->config->getSystemValue('logdateformat', 'c')); + $timestampInfo = $time->format($this->config->getSystemValue('logdateformat', \DateTime::ATOM)); return $timestampInfo . ' ' . $this->formatter->format($message); } diff --git a/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php b/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php index f9170e97a02..f172260df79 100644 --- a/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php +++ b/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php @@ -30,6 +30,8 @@ use OC\DB\QueryBuilder\Literal; use OC\DB\QueryBuilder\QueryFunction; use OC\DB\QueryBuilder\QuoteHelper; use OCP\DB\QueryBuilder\IExpressionBuilder; +use OCP\DB\QueryBuilder\ILiteral; +use OCP\DB\QueryBuilder\IQueryFunction; use OCP\IDBConnection; class ExpressionBuilder implements IExpressionBuilder { @@ -39,12 +41,16 @@ class ExpressionBuilder implements IExpressionBuilder { /** @var QuoteHelper */ protected $helper; + /** @var IDBConnection */ + protected $connection; + /** * Initializes a new <tt>ExpressionBuilder</tt>. * * @param \OCP\IDBConnection $connection */ public function __construct(IDBConnection $connection) { + $this->connection = $connection; $this->helper = new QuoteHelper(); $this->expressionBuilder = new DoctrineExpressionBuilder($connection); } @@ -345,12 +351,42 @@ class ExpressionBuilder implements IExpressionBuilder { } /** + * Binary AND Operator copies a bit to the result if it exists in both operands. + * + * @param string|ILiteral $x The field or value to check + * @param int $y Bitmap that must be set + * @return IQueryFunction + * @since 12.0.0 + */ + public function bitwiseAnd($x, $y) { + return new QueryFunction($this->connection->getDatabasePlatform()->getBitAndComparisonExpression( + $this->helper->quoteColumnName($x), + $y + )); + } + + /** + * Binary OR Operator copies a bit if it exists in either operand. + * + * @param string|ILiteral $x The field or value to check + * @param int $y Bitmap that must be set + * @return IQueryFunction + * @since 12.0.0 + */ + public function bitwiseOr($x, $y) { + return new QueryFunction($this->connection->getDatabasePlatform()->getBitOrComparisonExpression( + $this->helper->quoteColumnName($x), + $y + )); + } + + /** * Quotes a given input parameter. * * @param mixed $input The parameter to be quoted. * @param mixed|null $type One of the IQueryBuilder::PARAM_* constants * - * @return Literal + * @return ILiteral */ public function literal($input, $type = null) { return new Literal($this->expressionBuilder->literal($input, $type)); diff --git a/lib/private/Encryption/Keys/Storage.php b/lib/private/Encryption/Keys/Storage.php index 8149ffe9dce..e8d152581fe 100644 --- a/lib/private/Encryption/Keys/Storage.php +++ b/lib/private/Encryption/Keys/Storage.php @@ -51,6 +51,9 @@ class Storage implements IStorage { /** @var string */ private $encryption_base_dir; + /** @var string */ + private $backup_base_dir; + /** @var array */ private $keyCache = []; @@ -64,6 +67,7 @@ class Storage implements IStorage { $this->encryption_base_dir = '/files_encryption'; $this->keys_base_dir = $this->encryption_base_dir .'/keys'; + $this->backup_base_dir = $this->encryption_base_dir .'/backup'; $this->root_dir = $this->util->getKeyStorageRoot(); } @@ -287,6 +291,37 @@ class Storage implements IStorage { } /** + * backup keys of a given encryption module + * + * @param string $encryptionModuleId + * @param string $purpose + * @param string $uid + * @return bool + * @since 12.0.0 + */ + public function backupUserKeys($encryptionModuleId, $purpose, $uid) { + $source = $uid . $this->encryption_base_dir . '/' . $encryptionModuleId; + $backupDir = $uid . $this->backup_base_dir; + if (!$this->view->file_exists($backupDir)) { + $this->view->mkdir($backupDir); + } + + $backupDir = $backupDir . '/' . $purpose . '.' . $encryptionModuleId . '.' . $this->getTimestamp(); + $this->view->mkdir($backupDir); + + return $this->view->copy($source, $backupDir); + } + + /** + * get the current timestamp + * + * @return int + */ + protected function getTimestamp() { + return time(); + } + + /** * get system wide path and detect mount points * * @param string $path diff --git a/lib/private/Files/Cache/Cache.php b/lib/private/Files/Cache/Cache.php index 3a3f51488e6..a966d621c58 100644 --- a/lib/private/Files/Cache/Cache.php +++ b/lib/private/Files/Cache/Cache.php @@ -142,7 +142,7 @@ class Cache implements ICache { } return $data; } else { - return self::cacheEntryFromData($data, $this->storageId, $this->mimetypeLoader); + return self::cacheEntryFromData($data, $this->mimetypeLoader); } } @@ -150,11 +150,10 @@ class Cache implements ICache { * Create a CacheEntry from database row * * @param array $data - * @param string $storageId * @param IMimeTypeLoader $mimetypeLoader * @return CacheEntry */ - public static function cacheEntryFromData($data, $storageId, IMimeTypeLoader $mimetypeLoader) { + public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) { //fix types $data['fileid'] = (int)$data['fileid']; $data['parent'] = (int)$data['parent']; @@ -163,7 +162,7 @@ class Cache implements ICache { $data['storage_mtime'] = (int)$data['storage_mtime']; $data['encryptedVersion'] = (int)$data['encrypted']; $data['encrypted'] = (bool)$data['encrypted']; - $data['storage'] = $storageId; + $data['storage_id'] = $data['storage']; $data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']); $data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']); if ($data['storage_mtime'] == 0) { diff --git a/lib/private/Files/Config/UserMountCache.php b/lib/private/Files/Config/UserMountCache.php index c9a701d24b6..423eb5c423d 100644 --- a/lib/private/Files/Config/UserMountCache.php +++ b/lib/private/Files/Config/UserMountCache.php @@ -141,7 +141,11 @@ class UserMountCache implements IUserMountCache { foreach ($cachedMounts as $cachedMount) { if ( $newMount->getRootId() === $cachedMount->getRootId() && - ($newMount->getMountPoint() !== $cachedMount->getMountPoint() || $newMount->getMountId() !== $cachedMount->getMountId()) + ( + $newMount->getMountPoint() !== $cachedMount->getMountPoint() || + $newMount->getStorageId() !== $cachedMount->getStorageId() || + $newMount->getMountId() !== $cachedMount->getMountId() + ) ) { $changed[] = $newMount; } @@ -169,6 +173,7 @@ class UserMountCache implements IUserMountCache { $builder = $this->connection->getQueryBuilder(); $query = $builder->update('mounts') + ->set('storage_id', $builder->createNamedParameter($mount->getStorageId())) ->set('mount_point', $builder->createNamedParameter($mount->getMountPoint())) ->set('mount_id', $builder->createNamedParameter($mount->getMountId(), IQueryBuilder::PARAM_INT)) ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID()))) diff --git a/lib/private/Files/Mount/LocalHomeMountProvider.php b/lib/private/Files/Mount/LocalHomeMountProvider.php index 23bbfcd5ffa..9057f62995f 100644 --- a/lib/private/Files/Mount/LocalHomeMountProvider.php +++ b/lib/private/Files/Mount/LocalHomeMountProvider.php @@ -39,9 +39,6 @@ class LocalHomeMountProvider implements IHomeMountProvider { */ public function getHomeMountForUser(IUser $user, IStorageFactory $loader) { $arguments = ['user' => $user]; - if (\OC\Files\Cache\Storage::exists('local::' . $user->getHome() . '/')) { - $arguments['legacy'] = true; - } return new MountPoint('\OC\Files\Storage\Home', '/' . $user->getUID(), $arguments, $loader); } } diff --git a/lib/private/Files/Node/File.php b/lib/private/Files/Node/File.php index c4430b9181d..4bfa5d583f7 100644 --- a/lib/private/Files/Node/File.php +++ b/lib/private/Files/Node/File.php @@ -30,6 +30,16 @@ use OCP\Files\NotPermittedException; class File extends Node implements \OCP\Files\File { /** + * Creates a Folder that represents a non-existing path + * + * @param string $path path + * @return string non-existing node class + */ + protected function createNonExistingNode($path) { + return new NonExistingFile($this->root, $this->view, $path); + } + + /** * @return string * @throws \OCP\Files\NotPermittedException */ @@ -114,52 +124,6 @@ class File extends Node implements \OCP\Files\File { } /** - * @param string $targetPath - * @throws \OCP\Files\NotPermittedException - * @return \OC\Files\Node\Node - */ - public function copy($targetPath) { - $targetPath = $this->normalizePath($targetPath); - $parent = $this->root->get(dirname($targetPath)); - if ($parent instanceof Folder and $this->isValidPath($targetPath) and $parent->isCreatable()) { - $nonExisting = new NonExistingFile($this->root, $this->view, $targetPath); - $this->root->emit('\OC\Files', 'preCopy', array($this, $nonExisting)); - $this->root->emit('\OC\Files', 'preWrite', array($nonExisting)); - $this->view->copy($this->path, $targetPath); - $targetNode = $this->root->get($targetPath); - $this->root->emit('\OC\Files', 'postCopy', array($this, $targetNode)); - $this->root->emit('\OC\Files', 'postWrite', array($targetNode)); - return $targetNode; - } else { - throw new NotPermittedException(); - } - } - - /** - * @param string $targetPath - * @throws \OCP\Files\NotPermittedException - * @return \OC\Files\Node\Node - */ - public function move($targetPath) { - $targetPath = $this->normalizePath($targetPath); - $parent = $this->root->get(dirname($targetPath)); - if ($parent instanceof Folder and $this->isValidPath($targetPath) and $parent->isCreatable()) { - $nonExisting = new NonExistingFile($this->root, $this->view, $targetPath); - $this->root->emit('\OC\Files', 'preRename', array($this, $nonExisting)); - $this->root->emit('\OC\Files', 'preWrite', array($nonExisting)); - $this->view->rename($this->path, $targetPath); - $targetNode = $this->root->get($targetPath); - $this->root->emit('\OC\Files', 'postRename', array($this, $targetNode)); - $this->root->emit('\OC\Files', 'postWrite', array($targetNode)); - $this->path = $targetPath; - $this->fileInfo = null; - return $targetNode; - } else { - throw new NotPermittedException(); - } - } - - /** * @param string $type * @param bool $raw * @return string diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php index 288a02ef207..fd907f708f3 100644 --- a/lib/private/Files/Node/Folder.php +++ b/lib/private/Files/Node/Folder.php @@ -36,6 +36,16 @@ use OCP\Files\NotPermittedException; class Folder extends Node implements \OCP\Files\Folder { /** + * Creates a Folder that represents a non-existing path + * + * @param string $path path + * @return string non-existing node class + */ + protected function createNonExistingNode($path) { + return new NonExistingFolder($this->root, $this->view, $path); + } + + /** * @param string $path path relative to the folder * @return string * @throws \OCP\Files\NotPermittedException @@ -326,51 +336,6 @@ class Folder extends Node implements \OCP\Files\Folder { } /** - * @param string $targetPath - * @throws \OCP\Files\NotPermittedException - * @return \OC\Files\Node\Node - */ - public function copy($targetPath) { - $targetPath = $this->normalizePath($targetPath); - $parent = $this->root->get(dirname($targetPath)); - if ($parent instanceof Folder and $this->isValidPath($targetPath) and $parent->isCreatable()) { - $nonExisting = new NonExistingFolder($this->root, $this->view, $targetPath); - $this->root->emit('\OC\Files', 'preCopy', array($this, $nonExisting)); - $this->root->emit('\OC\Files', 'preWrite', array($nonExisting)); - $this->view->copy($this->path, $targetPath); - $targetNode = $this->root->get($targetPath); - $this->root->emit('\OC\Files', 'postCopy', array($this, $targetNode)); - $this->root->emit('\OC\Files', 'postWrite', array($targetNode)); - return $targetNode; - } else { - throw new NotPermittedException('No permission to copy to path'); - } - } - - /** - * @param string $targetPath - * @throws \OCP\Files\NotPermittedException - * @return \OC\Files\Node\Node - */ - public function move($targetPath) { - $targetPath = $this->normalizePath($targetPath); - $parent = $this->root->get(dirname($targetPath)); - if ($parent instanceof Folder and $this->isValidPath($targetPath) and $parent->isCreatable()) { - $nonExisting = new NonExistingFolder($this->root, $this->view, $targetPath); - $this->root->emit('\OC\Files', 'preRename', array($this, $nonExisting)); - $this->root->emit('\OC\Files', 'preWrite', array($nonExisting)); - $this->view->rename($this->path, $targetPath); - $targetNode = $this->root->get($targetPath); - $this->root->emit('\OC\Files', 'postRename', array($this, $targetNode)); - $this->root->emit('\OC\Files', 'postWrite', array($targetNode)); - $this->path = $targetPath; - return $targetNode; - } else { - throw new NotPermittedException('No permission to move to path'); - } - } - - /** * Add a suffix to the name in case the file exists * * @param string $name diff --git a/lib/private/Files/Node/Node.php b/lib/private/Files/Node/Node.php index 226c182622f..e00debe6903 100644 --- a/lib/private/Files/Node/Node.php +++ b/lib/private/Files/Node/Node.php @@ -33,6 +33,7 @@ use OCP\Files\InvalidPathException; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; +// FIXME: this class really should be abstract class Node implements \OCP\Files\Node { /** * @var \OC\Files\View $view @@ -56,7 +57,7 @@ class Node implements \OCP\Files\Node { /** * @param \OC\Files\View $view - * @param \OC\Files\Node\Root $root + * @param \OCP\Files\IRootFolder $root * @param string $path * @param FileInfo $fileInfo */ @@ -68,6 +69,16 @@ class Node implements \OCP\Files\Node { } /** + * Creates a Node of the same type that represents a non-existing path + * + * @param string $path path + * @return string non-existing node class + */ + protected function createNonExistingNode($path) { + throw new \Exception('Must be implemented by subclasses'); + } + + /** * Returns the matching file info * * @return FileInfo @@ -106,28 +117,11 @@ class Node implements \OCP\Files\Node { return ($this->getPermissions() & $permissions) === $permissions; } - /** - * @param string $targetPath - * @throws \OCP\Files\NotPermittedException - * @return \OC\Files\Node\Node - */ - public function move($targetPath) { - return; - } - public function delete() { return; } /** - * @param string $targetPath - * @return \OC\Files\Node\Node - */ - public function copy($targetPath) { - return; - } - - /** * @param int $mtime * @throws \OCP\Files\NotPermittedException */ @@ -381,4 +375,54 @@ class Node implements \OCP\Files\Node { public function unlock($type) { $this->view->unlockFile($this->path, $type); } + + /** + * @param string $targetPath + * @throws \OCP\Files\NotPermittedException if copy not allowed or failed + * @return \OC\Files\Node\Node + */ + public function copy($targetPath) { + $targetPath = $this->normalizePath($targetPath); + $parent = $this->root->get(dirname($targetPath)); + if ($parent instanceof Folder and $this->isValidPath($targetPath) and $parent->isCreatable()) { + $nonExisting = $this->createNonExistingNode($targetPath); + $this->root->emit('\OC\Files', 'preCopy', [$this, $nonExisting]); + $this->root->emit('\OC\Files', 'preWrite', [$nonExisting]); + if (!$this->view->copy($this->path, $targetPath)) { + throw new NotPermittedException('Could not copy ' . $this->path . ' to ' . $targetPath); + } + $targetNode = $this->root->get($targetPath); + $this->root->emit('\OC\Files', 'postCopy', [$this, $targetNode]); + $this->root->emit('\OC\Files', 'postWrite', [$targetNode]); + return $targetNode; + } else { + throw new NotPermittedException('No permission to copy to path ' . $targetPath); + } + } + + /** + * @param string $targetPath + * @throws \OCP\Files\NotPermittedException if move not allowed or failed + * @return \OC\Files\Node\Node + */ + public function move($targetPath) { + $targetPath = $this->normalizePath($targetPath); + $parent = $this->root->get(dirname($targetPath)); + if ($parent instanceof Folder and $this->isValidPath($targetPath) and $parent->isCreatable()) { + $nonExisting = $this->createNonExistingNode($targetPath); + $this->root->emit('\OC\Files', 'preRename', [$this, $nonExisting]); + $this->root->emit('\OC\Files', 'preWrite', [$nonExisting]); + if (!$this->view->rename($this->path, $targetPath)) { + throw new NotPermittedException('Could not move ' . $this->path . ' to ' . $targetPath); + } + $targetNode = $this->root->get($targetPath); + $this->root->emit('\OC\Files', 'postRename', [$this, $targetNode]); + $this->root->emit('\OC\Files', 'postWrite', [$targetNode]); + $this->path = $targetPath; + return $targetNode; + } else { + throw new NotPermittedException('No permission to move to path ' . $targetPath); + } + } + } diff --git a/lib/private/Files/Notify/Change.php b/lib/private/Files/Notify/Change.php new file mode 100644 index 00000000000..78cc007b27d --- /dev/null +++ b/lib/private/Files/Notify/Change.php @@ -0,0 +1,65 @@ +<?php +/** + * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> + * + * @author Robin Appelman <robin@icewind.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Files\Notify; + +use OCP\Files\Notify\IChange; + +class Change implements IChange { + /** @var int */ + private $type; + + /** @var string */ + private $path; + + /** + * Change constructor. + * + * @param int $type + * @param string $path + */ + public function __construct($type, $path) { + $this->type = $type; + $this->path = $path; + } + + /** + * Get the type of the change + * + * @return int IChange::ADDED, IChange::REMOVED, IChange::MODIFIED or IChange::RENAMED + */ + public function getType() { + return $this->type; + } + + /** + * Get the path of the file that was changed relative to the root of the storage + * + * Note, for rename changes this path is the old path for the file + * + * @return mixed + */ + public function getPath() { + return $this->path; + } +} diff --git a/lib/private/Files/Notify/RenameChange.php b/lib/private/Files/Notify/RenameChange.php new file mode 100644 index 00000000000..b83dffa0cb2 --- /dev/null +++ b/lib/private/Files/Notify/RenameChange.php @@ -0,0 +1,52 @@ +<?php +/** + * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> + * + * @author Robin Appelman <robin@icewind.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Files\Notify; + +use OCP\Files\Notify\IRenameChange; + +class RenameChange extends Change implements IRenameChange { + /** @var string */ + private $targetPath; + + /** + * Change constructor. + * + * @param int $type + * @param string $path + * @param string $targetPath + */ + public function __construct($type, $path, $targetPath) { + parent::__construct($type, $path); + $this->targetPath = $targetPath; + } + + /** + * Get the new path of the renamed file relative to the storage root + * + * @return string + */ + public function getTargetPath() { + return $this->targetPath; + } +} diff --git a/lib/private/Files/ObjectStore/ObjectStoreStorage.php b/lib/private/Files/ObjectStore/ObjectStoreStorage.php index 2dcf830cc1e..ab77c21e6c4 100644 --- a/lib/private/Files/ObjectStore/ObjectStoreStorage.php +++ b/lib/private/Files/ObjectStore/ObjectStoreStorage.php @@ -25,16 +25,12 @@ namespace OC\Files\ObjectStore; +use Icewind\Streams\CallbackWrapper; use Icewind\Streams\IteratorDirectory; use OC\Files\Cache\CacheEntry; use OCP\Files\ObjectStore\IObjectStore; class ObjectStoreStorage extends \OC\Files\Storage\Common { - - /** - * @var array - */ - private static $tmpFiles = array(); /** * @var \OCP\Files\ObjectStore\IObjectStore $objectStore */ @@ -291,14 +287,14 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common { $ext = ''; } $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext); - \OC\Files\Stream\Close::registerCallback($tmpFile, array($this, 'writeBack')); if ($this->file_exists($path)) { $source = $this->fopen($path, 'r'); file_put_contents($tmpFile, $source); } - self::$tmpFiles[$tmpFile] = $path; - - return fopen('close://' . $tmpFile, $mode); + $handle = fopen($tmpFile, $mode); + return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) { + $this->writeBack($tmpFile, $path); + }); } return false; } @@ -368,12 +364,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common { return true; } - public function writeBack($tmpFile) { - if (!isset(self::$tmpFiles[$tmpFile])) { - return; - } - - $path = self::$tmpFiles[$tmpFile]; + public function writeBack($tmpFile, $path) { $stat = $this->stat($path); if (empty($stat)) { // create new file diff --git a/lib/private/Files/Storage/Common.php b/lib/private/Files/Storage/Common.php index 5561f6a889b..f3e3cb9e58c 100644 --- a/lib/private/Files/Storage/Common.php +++ b/lib/private/Files/Storage/Common.php @@ -436,10 +436,14 @@ abstract class Common implements Storage, ILockingStorage { * @return bool */ public function test() { - if ($this->stat('')) { - return true; + try { + if ($this->stat('')) { + return true; + } + return false; + } catch (\Exception $e) { + return false; } - return false; } /** diff --git a/lib/private/Files/Storage/DAV.php b/lib/private/Files/Storage/DAV.php index ea4bbba2748..3b89a66d6a2 100644 --- a/lib/private/Files/Storage/DAV.php +++ b/lib/private/Files/Storage/DAV.php @@ -36,6 +36,7 @@ namespace OC\Files\Storage; use Exception; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Message\ResponseInterface; +use Icewind\Streams\CallbackWrapper; use OC\Files\Filesystem; use OC\Files\Stream\Close; use Icewind\Streams\IteratorDirectory; @@ -48,7 +49,6 @@ use OCP\Files\StorageInvalidException; use OCP\Files\StorageNotAvailableException; use OCP\Util; use Sabre\DAV\Client; -use Sabre\DAV\Exception\NotFound; use Sabre\DAV\Xml\Property\ResourceType; use Sabre\HTTP\ClientException; use Sabre\HTTP\ClientHttpException; @@ -77,8 +77,6 @@ class DAV extends Common { private $client; /** @var ArrayCache */ private $statCache; - /** @var array */ - private static $tempFiles = []; /** @var \OCP\Http\Client\IClientService */ private $httpClientService; @@ -203,13 +201,15 @@ class DAV extends Common { $this->init(); $path = $this->cleanPath($path); try { - $response = $this->client->propfind( + $response = $this->client->propFind( $this->encodePath($path), - array(), + [], 1 ); - $id = md5('webdav' . $this->root . $path); - $content = array(); + if ($response === false) { + return false; + } + $content = []; $files = array_keys($response); array_shift($files); //the first entry is the current directory @@ -226,13 +226,6 @@ class DAV extends Common { $content[] = $file; } return IteratorDirectory::wrap($content); - } catch (ClientHttpException $e) { - if ($e->getHttpStatus() === 404) { - $this->statCache->clear($path . '/'); - $this->statCache->set($path, false); - return false; - } - $this->convertException($e, $path); } catch (\Exception $e) { $this->convertException($e, $path); } @@ -247,22 +240,18 @@ class DAV extends Common { * * @param string $path path to propfind * - * @return array propfind response + * @return array|boolean propfind response or false if the entry was not found * - * @throws NotFound + * @throws ClientHttpException */ protected function propfind($path) { $path = $this->cleanPath($path); $cachedResponse = $this->statCache->get($path); - if ($cachedResponse === false) { - // we know it didn't exist - throw new NotFound(); - } // we either don't know it, or we know it exists but need more details if (is_null($cachedResponse) || $cachedResponse === true) { $this->init(); try { - $response = $this->client->propfind( + $response = $this->client->propFind( $this->encodePath($path), array( '{DAV:}getlastmodified', @@ -275,11 +264,15 @@ class DAV extends Common { ) ); $this->statCache->set($path, $response); - } catch (NotFound $e) { - // remember that this path did not exist - $this->statCache->clear($path . '/'); - $this->statCache->set($path, false); - throw $e; + } catch (ClientHttpException $e) { + if ($e->getHttpStatus() === 404) { + $this->statCache->clear($path . '/'); + $this->statCache->set($path, false); + return false; + } + $this->convertException($e, $path); + } catch (\Exception $e) { + $this->convertException($e, $path); } } else { $response = $cachedResponse; @@ -291,17 +284,15 @@ class DAV extends Common { public function filetype($path) { try { $response = $this->propfind($path); - $responseType = array(); + if ($response === false) { + return false; + } + $responseType = []; if (isset($response["{DAV:}resourcetype"])) { /** @var ResourceType[] $response */ $responseType = $response["{DAV:}resourcetype"]->getValue(); } return (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file'; - } catch (ClientHttpException $e) { - if ($e->getHttpStatus() === 404) { - return false; - } - $this->convertException($e, $path); } catch (\Exception $e) { $this->convertException($e, $path); } @@ -320,13 +311,7 @@ class DAV extends Common { return true; } // need to get from server - $this->propfind($path); - return true; //no 404 exception - } catch (ClientHttpException $e) { - if ($e->getHttpStatus() === 404) { - return false; - } - $this->convertException($e, $path); + return ($this->propfind($path) !== false); } catch (\Exception $e) { $this->convertException($e, $path); } @@ -409,20 +394,19 @@ class DAV extends Common { } $tmpFile = $tempManager->getTemporaryFile($ext); } - Close::registerCallback($tmpFile, array($this, 'writeBack')); - self::$tempFiles[$tmpFile] = $path; - return fopen('close://' . $tmpFile, $mode); + $handle = fopen($tmpFile, $mode); + return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) { + $this->writeBack($tmpFile, $path); + }); } } /** * @param string $tmpFile */ - public function writeBack($tmpFile) { - if (isset(self::$tempFiles[$tmpFile])) { - $this->uploadFile($tmpFile, self::$tempFiles[$tmpFile]); - unlink($tmpFile); - } + public function writeBack($tmpFile, $path) { + $this->uploadFile($tmpFile, $path); + unlink($tmpFile); } /** {@inheritdoc} */ @@ -431,7 +415,10 @@ class DAV extends Common { $path = $this->cleanPath($path); try { // TODO: cacheable ? - $response = $this->client->propfind($this->encodePath($path), array('{DAV:}quota-available-bytes')); + $response = $this->client->propfind($this->encodePath($path), ['{DAV:}quota-available-bytes']); + if ($response === false) { + return FileInfo::SPACE_UNKNOWN; + } if (isset($response['{DAV:}quota-available-bytes'])) { return (int)$response['{DAV:}quota-available-bytes']; } else { @@ -457,6 +444,9 @@ class DAV extends Common { $this->client->proppatch($this->encodePath($path), ['{DAV:}lastmodified' => $mtime]); // non-owncloud clients might not have accepted the property, need to recheck it $response = $this->client->propfind($this->encodePath($path), ['{DAV:}getlastmodified'], 0); + if ($response === false) { + return false; + } if (isset($response['{DAV:}getlastmodified'])) { $remoteMtime = strtotime($response['{DAV:}getlastmodified']); if ($remoteMtime !== $mtime) { @@ -579,15 +569,13 @@ class DAV extends Common { public function stat($path) { try { $response = $this->propfind($path); - return array( + if (!$response) { + return false; + } + return [ 'mtime' => strtotime($response['{DAV:}getlastmodified']), 'size' => (int)isset($response['{DAV:}getcontentlength']) ? $response['{DAV:}getcontentlength'] : 0, - ); - } catch (ClientHttpException $e) { - if ($e->getHttpStatus() === 404) { - return array(); - } - $this->convertException($e, $path); + ]; } catch (\Exception $e) { $this->convertException($e, $path); } @@ -598,7 +586,10 @@ class DAV extends Common { public function getMimeType($path) { try { $response = $this->propfind($path); - $responseType = array(); + if ($response === false) { + return false; + } + $responseType = []; if (isset($response["{DAV:}resourcetype"])) { /** @var ResourceType[] $response */ $responseType = $response["{DAV:}resourcetype"]->getValue(); @@ -611,11 +602,6 @@ class DAV extends Common { } else { return false; } - } catch (ClientHttpException $e) { - if ($e->getHttpStatus() === 404) { - return false; - } - $this->convertException($e, $path); } catch (\Exception $e) { $this->convertException($e, $path); } @@ -706,6 +692,9 @@ class DAV extends Common { $this->init(); $path = $this->cleanPath($path); $response = $this->propfind($path); + if ($response === false) { + return 0; + } if (isset($response['{http://owncloud.org/ns}permissions'])) { return $this->parsePermissions($response['{http://owncloud.org/ns}permissions']); } else if ($this->is_dir($path)) { @@ -722,6 +711,9 @@ class DAV extends Common { $this->init(); $path = $this->cleanPath($path); $response = $this->propfind($path); + if ($response === false) { + return null; + } if (isset($response['{DAV:}getetag'])) { return trim($response['{DAV:}getetag'], '"'); } @@ -765,6 +757,13 @@ class DAV extends Common { // force refresh for $path $this->statCache->remove($path); $response = $this->propfind($path); + if ($response === false) { + if ($path === '') { + // if root is gone it means the storage is not available + throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage()); + } + return false; + } if (isset($response['{DAV:}getetag'])) { $cachedData = $this->getCache()->get($path); $etag = null; @@ -787,7 +786,7 @@ class DAV extends Common { return $remoteMtime > $time; } } catch (ClientHttpException $e) { - if ($e->getHttpStatus() === 404 || $e->getHttpStatus() === 405) { + if ($e->getHttpStatus() === 405) { if ($path === '') { // if root is gone it means the storage is not available throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage()); diff --git a/lib/private/Files/Storage/Home.php b/lib/private/Files/Storage/Home.php index e5ba0f9dfe4..57b32349324 100644 --- a/lib/private/Files/Storage/Home.php +++ b/lib/private/Files/Storage/Home.php @@ -44,19 +44,12 @@ class Home extends Local implements \OCP\Files\IHomeStorage { /** * Construct a Home storage instance * @param array $arguments array with "user" containing the - * storage owner and "legacy" containing "true" if the storage is - * a legacy storage with "local::" URL instead of the new "home::" one. + * storage owner */ public function __construct($arguments) { $this->user = $arguments['user']; $datadir = $this->user->getHome(); - if (isset($arguments['legacy']) && $arguments['legacy']) { - // legacy home id (<= 5.0.12) - $this->id = 'local::' . $datadir . '/'; - } - else { - $this->id = 'home::' . $this->user->getUID(); - } + $this->id = 'home::' . $this->user->getUID(); parent::__construct(array('datadir' => $datadir)); } diff --git a/lib/private/Files/Storage/Local.php b/lib/private/Files/Storage/Local.php index 4fe7dcafbbf..80d48680be1 100644 --- a/lib/private/Files/Storage/Local.php +++ b/lib/private/Files/Storage/Local.php @@ -205,18 +205,7 @@ class Local extends \OC\Files\Storage\Common { } public function file_get_contents($path) { - // file_get_contents() has a memory leak: https://bugs.php.net/bug.php?id=61961 - $fileName = $this->getSourcePath($path); - - $fileSize = filesize($fileName); - if ($fileSize === 0) { - return ''; - } - - $handle = fopen($fileName, 'rb'); - $content = fread($handle, $fileSize); - fclose($handle); - return $content; + return file_get_contents($this->getSourcePath($path)); } public function file_put_contents($path, $data) { diff --git a/lib/private/Files/Stream/Close.php b/lib/private/Files/Stream/Close.php deleted file mode 100644 index 7cc9903c912..00000000000 --- a/lib/private/Files/Stream/Close.php +++ /dev/null @@ -1,119 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Files\Stream; - -/** - * stream wrapper that provides a callback on stream close - */ -class Close { - private static $callBacks = array(); - private $path = ''; - private $source; - private static $open = array(); - - public function stream_open($path, $mode, $options, &$opened_path) { - $path = substr($path, strlen('close://')); - $this->path = $path; - $this->source = fopen($path, $mode); - if (is_resource($this->source)) { - $this->meta = stream_get_meta_data($this->source); - } - self::$open[] = $path; - return is_resource($this->source); - } - - public function stream_seek($offset, $whence = SEEK_SET) { - return fseek($this->source, $offset, $whence) === 0; - } - - public function stream_tell() { - return ftell($this->source); - } - - public function stream_read($count) { - return fread($this->source, $count); - } - - public function stream_write($data) { - return fwrite($this->source, $data); - } - - public function stream_set_option($option, $arg1, $arg2) { - switch ($option) { - case STREAM_OPTION_BLOCKING: - stream_set_blocking($this->source, $arg1); - break; - case STREAM_OPTION_READ_TIMEOUT: - stream_set_timeout($this->source, $arg1, $arg2); - break; - case STREAM_OPTION_WRITE_BUFFER: - stream_set_write_buffer($this->source, $arg1, $arg2); - } - } - - public function stream_stat() { - return fstat($this->source); - } - - public function stream_lock($mode) { - flock($this->source, $mode); - } - - public function stream_flush() { - return fflush($this->source); - } - - public function stream_eof() { - return feof($this->source); - } - - public function url_stat($path) { - $path = substr($path, strlen('close://')); - if (file_exists($path)) { - return stat($path); - } else { - return false; - } - } - - public function stream_close() { - fclose($this->source); - if (isset(self::$callBacks[$this->path])) { - call_user_func(self::$callBacks[$this->path], $this->path); - } - } - - public function unlink($path) { - $path = substr($path, strlen('close://')); - return unlink($path); - } - - /** - * @param string $path - */ - public static function registerCallback($path, $callback) { - self::$callBacks[$path] = $callback; - } -} diff --git a/lib/private/Files/Stream/Dir.php b/lib/private/Files/Stream/Dir.php deleted file mode 100644 index f8f6b1fa89a..00000000000 --- a/lib/private/Files/Stream/Dir.php +++ /dev/null @@ -1,67 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Files\Stream; - -class Dir { - private static $dirs = array(); - private $name; - private $index; - - public function dir_opendir($path, $options) { - $this->name = substr($path, strlen('fakedir://')); - $this->index = 0; - if (!isset(self::$dirs[$this->name])) { - self::$dirs[$this->name] = array(); - } - return true; - } - - public function dir_readdir() { - if ($this->index >= count(self::$dirs[$this->name])) { - return false; - } - $filename = self::$dirs[$this->name][$this->index]; - $this->index++; - return $filename; - } - - public function dir_closedir() { - $this->name = ''; - return true; - } - - public function dir_rewinddir() { - $this->index = 0; - return true; - } - - /** - * @param string $path - * @param string[] $content - */ - public static function register($path, $content) { - self::$dirs[$path] = $content; - } -} diff --git a/lib/private/Files/Stream/Quota.php b/lib/private/Files/Stream/Quota.php index f064ca6c011..624a2021b9c 100644 --- a/lib/private/Files/Stream/Quota.php +++ b/lib/private/Files/Stream/Quota.php @@ -25,61 +25,44 @@ namespace OC\Files\Stream; +use Icewind\Streams\Wrapper; + /** * stream wrapper limits the amount of data that can be written to a stream * - * usage: void \OC\Files\Stream\Quota::register($id, $stream, $limit) - * or: resource \OC\Files\Stream\Quota::wrap($stream, $limit) + * usage: resource \OC\Files\Stream\Quota::wrap($stream, $limit) */ -class Quota { - private static $streams = array(); - - /** - * @var resource $source - */ - private $source; - +class Quota extends Wrapper { /** * @var int $limit */ private $limit; /** - * @param string $id - * @param resource $stream - * @param int $limit - */ - public static function register($id, $stream, $limit) { - self::$streams[$id] = array($stream, $limit); - } - - /** - * remove all registered streams - */ - public static function clear() { - self::$streams = array(); - } - - /** * @param resource $stream * @param int $limit * @return resource */ static public function wrap($stream, $limit) { - $id = uniqid(); - self::register($id, $stream, $limit); - $meta = stream_get_meta_data($stream); - return fopen('quota://' . $id, $meta['mode']); + $context = stream_context_create(array( + 'quota' => array( + 'source' => $stream, + 'limit' => $limit + ) + )); + return Wrapper::wrapSource($stream, $context, 'quota', self::class); } public function stream_open($path, $mode, $options, &$opened_path) { - $id = substr($path, strlen('quota://')); - if (isset(self::$streams[$id])) { - list($this->source, $this->limit) = self::$streams[$id]; - return true; - } else { - return false; - } + $context = $this->loadContext('quota'); + $this->source = $context['source']; + $this->limit = $context['limit']; + + return true; + } + + public function dir_opendir($path, $options) { + return false; } public function stream_seek($offset, $whence = SEEK_SET) { @@ -103,10 +86,6 @@ class Quota { return fseek($this->source, $offset, $whence) === 0; } - public function stream_tell() { - return ftell($this->source); - } - public function stream_read($count) { $this->limit -= $count; return fread($this->source, $count); @@ -121,37 +100,4 @@ class Quota { $this->limit -= $size; return fwrite($this->source, $data); } - - public function stream_set_option($option, $arg1, $arg2) { - switch ($option) { - case STREAM_OPTION_BLOCKING: - stream_set_blocking($this->source, $arg1); - break; - case STREAM_OPTION_READ_TIMEOUT: - stream_set_timeout($this->source, $arg1, $arg2); - break; - case STREAM_OPTION_WRITE_BUFFER: - stream_set_write_buffer($this->source, $arg1, $arg2); - } - } - - public function stream_stat() { - return fstat($this->source); - } - - public function stream_lock($mode) { - return flock($this->source, $mode); - } - - public function stream_flush() { - return fflush($this->source); - } - - public function stream_eof() { - return feof($this->source); - } - - public function stream_close() { - fclose($this->source); - } } diff --git a/lib/private/Files/Stream/StaticStream.php b/lib/private/Files/Stream/StaticStream.php deleted file mode 100644 index 3c91b23fd36..00000000000 --- a/lib/private/Files/Stream/StaticStream.php +++ /dev/null @@ -1,171 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Files\Stream; - -class StaticStream { - const MODE_FILE = 0100000; - - public $context; - protected static $data = array(); - - protected $path = ''; - protected $pointer = 0; - protected $writable = false; - - public function stream_close() { - } - - public function stream_eof() { - return $this->pointer >= strlen(self::$data[$this->path]); - } - - public function stream_flush() { - } - - public static function clear() { - self::$data = array(); - } - - public function stream_open($path, $mode, $options, &$opened_path) { - switch ($mode[0]) { - case 'r': - if (!isset(self::$data[$path])) return false; - $this->path = $path; - $this->writable = isset($mode[1]) && $mode[1] == '+'; - break; - case 'w': - self::$data[$path] = ''; - $this->path = $path; - $this->writable = true; - break; - case 'a': - if (!isset(self::$data[$path])) self::$data[$path] = ''; - $this->path = $path; - $this->writable = true; - $this->pointer = strlen(self::$data[$path]); - break; - case 'x': - if (isset(self::$data[$path])) return false; - $this->path = $path; - $this->writable = true; - break; - case 'c': - if (!isset(self::$data[$path])) self::$data[$path] = ''; - $this->path = $path; - $this->writable = true; - break; - default: - return false; - } - $opened_path = $this->path; - return true; - } - - public function stream_read($count) { - $bytes = min(strlen(self::$data[$this->path]) - $this->pointer, $count); - $data = substr(self::$data[$this->path], $this->pointer, $bytes); - $this->pointer += $bytes; - return $data; - } - - public function stream_seek($offset, $whence = SEEK_SET) { - $len = strlen(self::$data[$this->path]); - switch ($whence) { - case SEEK_SET: - if ($offset <= $len) { - $this->pointer = $offset; - return true; - } - break; - case SEEK_CUR: - if ($this->pointer + $offset <= $len) { - $this->pointer += $offset; - return true; - } - break; - case SEEK_END: - if ($len + $offset <= $len) { - $this->pointer = $len + $offset; - return true; - } - break; - } - return false; - } - - public function stream_stat() { - return $this->url_stat($this->path); - } - - public function stream_tell() { - return $this->pointer; - } - - public function stream_write($data) { - if (!$this->writable) return 0; - $size = strlen($data); - if ($this->stream_eof()) { - self::$data[$this->path] .= $data; - } else { - self::$data[$this->path] = substr_replace( - self::$data[$this->path], - $data, - $this->pointer - ); - } - $this->pointer += $size; - return $size; - } - - public function unlink($path) { - if (isset(self::$data[$path])) { - unset(self::$data[$path]); - } - return true; - } - - public function url_stat($path) { - if (isset(self::$data[$path])) { - $size = strlen(self::$data[$path]); - $time = time(); - $data = array( - 'dev' => 0, - 'ino' => 0, - 'mode' => self::MODE_FILE | 0777, - 'nlink' => 1, - 'uid' => 0, - 'gid' => 0, - 'rdev' => '', - 'size' => $size, - 'atime' => $time, - 'mtime' => $time, - 'ctime' => $time, - 'blksize' => -1, - 'blocks' => -1, - ); - return array_values($data) + $data; - } - return false; - } -} diff --git a/lib/private/Files/Type/Detection.php b/lib/private/Files/Type/Detection.php index 84d727ebb0e..cd4ddc2f067 100644 --- a/lib/private/Files/Type/Detection.php +++ b/lib/private/Files/Type/Detection.php @@ -167,6 +167,10 @@ class Detection implements IMimeTypeDetector { $this->loadMappings(); $fileName = basename($path); + + // remove leading dot on hidden files with a file extension + $fileName = ltrim($fileName, '.'); + // note: leading dot doesn't qualify as extension if (strpos($fileName, '.') > 0) { //try to guess the type by the file extension diff --git a/lib/private/Files/Utils/Scanner.php b/lib/private/Files/Utils/Scanner.php index d26c601be1a..98e2c3c8ca2 100644 --- a/lib/private/Files/Utils/Scanner.php +++ b/lib/private/Files/Utils/Scanner.php @@ -31,6 +31,7 @@ use OC\Files\Filesystem; use OC\ForbiddenException; use OC\Hooks\PublicEmitter; use OC\Lock\DBLockingProvider; +use OCA\Files_Sharing\SharedStorage; use OCP\Files\Storage\IStorage; use OCP\Files\StorageNotAvailableException; use OCP\ILogger; @@ -118,14 +119,19 @@ class Scanner extends PublicEmitter { public function backgroundScan($dir) { $mounts = $this->getMounts($dir); foreach ($mounts as $mount) { - if (is_null($mount->getStorage())) { + $storage = $mount->getStorage(); + if (is_null($storage)) { continue; } // don't scan the root storage - if ($mount->getStorage()->instanceOfStorage('\OC\Files\Storage\Local') && $mount->getMountPoint() === '/') { + if ($storage->instanceOfStorage('\OC\Files\Storage\Local') && $mount->getMountPoint() === '/') { + continue; + } + + // don't scan received local shares, these can be scanned when scanning the owner's storage + if ($storage->instanceOfStorage(SharedStorage::class)) { continue; } - $storage = $mount->getStorage(); $scanner = $storage->getScanner(); $this->attachListener($mount); @@ -156,10 +162,10 @@ class Scanner extends PublicEmitter { } $mounts = $this->getMounts($dir); foreach ($mounts as $mount) { - if (is_null($mount->getStorage())) { + $storage = $mount->getStorage(); + if (is_null($storage)) { continue; } - $storage = $mount->getStorage(); // if the home storage isn't writable then the scanner is run as the wrong user if ($storage->instanceOfStorage('\OC\Files\Storage\Home') and (!$storage->isCreatable('') or !$storage->isCreatable('files')) @@ -171,6 +177,11 @@ class Scanner extends PublicEmitter { } } + + // don't scan received local shares, these can be scanned when scanning the owner's storage + if ($storage->instanceOfStorage(SharedStorage::class)) { + continue; + } $relativePath = $mount->getInternalPath($dir); $scanner = $storage->getScanner(); $scanner->setUseTransactions(false); diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index 6facc7b9462..db21d400b39 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -931,39 +931,36 @@ class View { /** * @param string $path - * @param string $mode + * @param string $mode 'r' or 'w' * @return resource */ public function fopen($path, $mode) { + $mode = str_replace('b', '', $mode); // the binary flag is a windows only feature which we do not support $hooks = array(); switch ($mode) { case 'r': - case 'rb': $hooks[] = 'read'; break; case 'r+': - case 'rb+': case 'w+': - case 'wb+': case 'x+': - case 'xb+': case 'a+': - case 'ab+': $hooks[] = 'read'; $hooks[] = 'write'; break; case 'w': - case 'wb': case 'x': - case 'xb': case 'a': - case 'ab': $hooks[] = 'write'; break; default: \OCP\Util::writeLog('core', 'invalid mode (' . $mode . ') for ' . $path, \OCP\Util::ERROR); } + if ($mode !== 'r' && $mode !== 'w') { + \OC::$server->getLogger()->info('Trying to open a file with a mode other than "r" or "w" can cause severe performance issues with some backends'); + } + return $this->basicOperation('fopen', $path, $hooks, $mode); } @@ -1005,7 +1002,7 @@ class View { // Create the directories if any if (!$this->file_exists($filePath)) { $result = $this->createParentDirectories($filePath); - if($result === false) { + if ($result === false) { return false; } } @@ -1149,6 +1146,8 @@ class View { $unlockLater = false; if ($this->lockingEnabled && $operation === 'fopen' && is_resource($result)) { $unlockLater = true; + // make sure our unlocking callback will still be called if connection is aborted + ignore_user_abort(true); $result = CallbackWrapper::wrap($result, null, null, function () use ($hooks, $path) { if (in_array('write', $hooks)) { $this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE); @@ -1357,7 +1356,7 @@ class View { //add the sizes of other mount points to the folder $extOnly = ($includeMountPoints === 'ext'); $mounts = Filesystem::getMountManager()->findIn($path); - $info->setSubMounts(array_filter($mounts, function(IMountPoint $mount) use ($extOnly) { + $info->setSubMounts(array_filter($mounts, function (IMountPoint $mount) use ($extOnly) { $subStorage = $mount->getStorage(); return !($extOnly && $subStorage instanceof \OCA\Files_Sharing\SharedStorage); })); @@ -2106,13 +2105,13 @@ class View { private function createParentDirectories($filePath) { $directoryParts = explode('/', $filePath); $directoryParts = array_filter($directoryParts); - foreach($directoryParts as $key => $part) { + foreach ($directoryParts as $key => $part) { $currentPathElements = array_slice($directoryParts, 0, $key); $currentPath = '/' . implode('/', $currentPathElements); - if($this->is_file($currentPath)) { + if ($this->is_file($currentPath)) { return false; } - if(!$this->file_exists($currentPath)) { + if (!$this->file_exists($currentPath)) { $this->mkdir($currentPath); } } diff --git a/lib/private/Group/Backend.php b/lib/private/Group/Backend.php index 14c36d93422..1e8d62f5e42 100644 --- a/lib/private/Group/Backend.php +++ b/lib/private/Group/Backend.php @@ -31,23 +31,14 @@ abstract class Backend implements \OCP\GroupInterface { */ const NOT_IMPLEMENTED = -501; - /** - * actions that user backends can define - */ - const CREATE_GROUP = 0x00000001; - const DELETE_GROUP = 0x00000010; - const ADD_TO_GROUP = 0x00000100; - const REMOVE_FROM_GOUP = 0x00001000; - //OBSOLETE const GET_DISPLAYNAME = 0x00010000; - const COUNT_USERS = 0x00100000; - - protected $possibleActions = array( + protected $possibleActions = [ self::CREATE_GROUP => 'createGroup', self::DELETE_GROUP => 'deleteGroup', self::ADD_TO_GROUP => 'addToGroup', self::REMOVE_FROM_GOUP => 'removeFromGroup', self::COUNT_USERS => 'countUsersInGroup', - ); + self::GROUP_DETAILS => 'getGroupDetails', + ]; /** * Get all supported actions diff --git a/lib/private/Group/Database.php b/lib/private/Group/Database.php index e4144fdbe20..8be24fc50de 100644 --- a/lib/private/Group/Database.php +++ b/lib/private/Group/Database.php @@ -202,6 +202,11 @@ class Database extends \OC\Group\Backend { * if the user exists at all. */ public function getUserGroups( $uid ) { + //guests has empty or null $uid + if ($uid === null || $uid === '') { + return []; + } + $this->fixDI(); // No magic! diff --git a/lib/private/Group/Group.php b/lib/private/Group/Group.php index 9379d604c48..69dce215694 100644 --- a/lib/private/Group/Group.php +++ b/lib/private/Group/Group.php @@ -31,6 +31,9 @@ namespace OC\Group; use OCP\IGroup; class Group implements IGroup { + /** @var null|string */ + protected $displayName; + /** * @var string $id */ @@ -66,18 +69,27 @@ class Group implements IGroup { * @param \OC\Group\Backend[] $backends * @param \OC\User\Manager $userManager * @param \OC\Hooks\PublicEmitter $emitter + * @param string $displayName */ - public function __construct($gid, $backends, $userManager, $emitter = null) { + public function __construct($gid, $backends, $userManager, $emitter = null, $displayName = null) { $this->gid = $gid; $this->backends = $backends; $this->userManager = $userManager; $this->emitter = $emitter; + $this->displayName = $displayName; } public function getGID() { return $this->gid; } + public function getDisplayName() { + if (is_null($this->displayName)) { + return $this->gid; + } + return $this->displayName; + } + /** * get all users in the group * diff --git a/lib/private/Group/Manager.php b/lib/private/Group/Manager.php index 31911138985..944598a8296 100644 --- a/lib/private/Group/Manager.php +++ b/lib/private/Group/Manager.php @@ -155,19 +155,29 @@ class Manager extends PublicEmitter implements IGroupManager { /** * @param string $gid + * @param string $displayName * @return \OCP\IGroup */ - protected function getGroupObject($gid) { + protected function getGroupObject($gid, $displayName = null) { $backends = array(); foreach ($this->backends as $backend) { - if ($backend->groupExists($gid)) { + if ($backend->implementsActions(\OC\Group\Backend::GROUP_DETAILS)) { + $groupData = $backend->getGroupDetails($gid); + if (is_array($groupData)) { + // take the display name from the first backend that has a non-null one + if (is_null($displayName) && isset($groupData['displayName'])) { + $displayName = $groupData['displayName']; + } + $backends[] = $backend; + } + } else if ($backend->groupExists($gid)) { $backends[] = $backend; } } if (count($backends) === 0) { return null; } - $this->cachedGroups[$gid] = new Group($gid, $backends, $this->userManager, $this); + $this->cachedGroups[$gid] = new Group($gid, $backends, $this->userManager, $this, $displayName); return $this->cachedGroups[$gid]; } diff --git a/lib/private/Http/Client/Client.php b/lib/private/Http/Client/Client.php index 8e182111303..4697f2e038c 100644 --- a/lib/private/Http/Client/Client.php +++ b/lib/private/Http/Client/Client.php @@ -77,9 +77,10 @@ class Client implements IClient { } } - $this->client->setDefaultOption('headers/User-Agent', 'ownCloud Server Crawler'); - if ($this->getProxyUri() !== '') { - $this->client->setDefaultOption('proxy', $this->getProxyUri()); + $this->client->setDefaultOption('headers/User-Agent', 'Nextcloud Server Crawler'); + $proxyUri = $this->getProxyUri(); + if ($proxyUri !== '') { + $this->client->setDefaultOption('proxy', $proxyUri); } } @@ -93,10 +94,10 @@ class Client implements IClient { $proxyUserPwd = $this->config->getSystemValue('proxyuserpwd', null); $proxyUri = ''; - if (!is_null($proxyUserPwd)) { + if ($proxyUserPwd !== null) { $proxyUri .= $proxyUserPwd . '@'; } - if (!is_null($proxyHost)) { + if ($proxyHost !== null) { $proxyUri .= $proxyHost; } diff --git a/lib/private/IntegrityCheck/Checker.php b/lib/private/IntegrityCheck/Checker.php index 102fe42a99d..419f989fa0f 100644 --- a/lib/private/IntegrityCheck/Checker.php +++ b/lib/private/IntegrityCheck/Checker.php @@ -96,12 +96,8 @@ class Checker { * @return bool */ public function isCodeCheckEnforced() { - $signedChannels = [ - 'daily', - 'testing', - 'stable', - ]; - if(!in_array($this->environmentHelper->getChannel(), $signedChannels, true)) { + $notSignedChannels = [ '', 'git']; + if (in_array($this->environmentHelper->getChannel(), $notSignedChannels, true)) { return false; } @@ -115,7 +111,7 @@ class Checker { } else { $isIntegrityCheckDisabled = false; } - if($isIntegrityCheckDisabled === true) { + if ($isIntegrityCheckDisabled === true) { return false; } @@ -271,16 +267,23 @@ class Checker { public function writeAppSignature($path, X509 $certificate, RSA $privateKey) { - if(!is_dir($path)) { - throw new \Exception('Directory does not exist.'); - } - $iterator = $this->getFolderIterator($path); - $hashes = $this->generateHashes($iterator, $path); - $signature = $this->createSignatureData($hashes, $certificate, $privateKey); - $this->fileAccessHelper->file_put_contents( - $path . '/appinfo/signature.json', + $appInfoDir = $path . '/appinfo'; + try { + $this->fileAccessHelper->assertDirectoryExists($appInfoDir); + + $iterator = $this->getFolderIterator($path); + $hashes = $this->generateHashes($iterator, $path); + $signature = $this->createSignatureData($hashes, $certificate, $privateKey); + $this->fileAccessHelper->file_put_contents( + $appInfoDir . '/signature.json', json_encode($signature, JSON_PRETTY_PRINT) - ); + ); + } catch (\Exception $e){ + if (!$this->fileAccessHelper->is_writable($appInfoDir)) { + throw new \Exception($appInfoDir . ' is not writable'); + } + throw $e; + } } /** @@ -289,17 +292,28 @@ class Checker { * @param X509 $certificate * @param RSA $rsa * @param string $path + * @throws \Exception */ public function writeCoreSignature(X509 $certificate, RSA $rsa, $path) { - $iterator = $this->getFolderIterator($path, $path); - $hashes = $this->generateHashes($iterator, $path); - $signatureData = $this->createSignatureData($hashes, $certificate, $rsa); - $this->fileAccessHelper->file_put_contents( - $path . '/core/signature.json', + $coreDir = $path . '/core'; + try { + + $this->fileAccessHelper->assertDirectoryExists($coreDir); + $iterator = $this->getFolderIterator($path, $path); + $hashes = $this->generateHashes($iterator, $path); + $signatureData = $this->createSignatureData($hashes, $certificate, $rsa); + $this->fileAccessHelper->file_put_contents( + $coreDir . '/signature.json', json_encode($signatureData, JSON_PRETTY_PRINT) - ); + ); + } catch (\Exception $e){ + if (!$this->fileAccessHelper->is_writable($coreDir)) { + throw new \Exception($coreDir . ' is not writable'); + } + throw $e; + } } /** diff --git a/lib/private/IntegrityCheck/Helpers/FileAccessHelper.php b/lib/private/IntegrityCheck/Helpers/FileAccessHelper.php index 9e2b76ce11a..a7e378c165e 100644 --- a/lib/private/IntegrityCheck/Helpers/FileAccessHelper.php +++ b/lib/private/IntegrityCheck/Helpers/FileAccessHelper.php @@ -53,10 +53,33 @@ class FileAccessHelper { * Wrapper around file_put_contents($filename, $data) * * @param string $filename - * @param $data - * @return int|false + * @param string $data + * @return int + * @throws \Exception */ public function file_put_contents($filename, $data) { - return file_put_contents($filename, $data); + $bytesWritten = @file_put_contents($filename, $data); + if ($bytesWritten === false || $bytesWritten !== strlen($data)){ + throw new \Exception('Failed to write into ' . $filename); + } + return $bytesWritten; + } + + /** + * @param string $path + * @return bool + */ + public function is_writable($path) { + return is_writable($path); + } + + /** + * @param string $path + * @throws \Exception + */ + public function assertDirectoryExists($path) { + if (!is_dir($path)) { + throw new \Exception('Directory ' . $path . ' does not exist.'); + } } } diff --git a/lib/private/Log.php b/lib/private/Log.php index ef1b70d3cb9..fddd3593127 100644 --- a/lib/private/Log.php +++ b/lib/private/Log.php @@ -106,12 +106,8 @@ class Log implements ILogger { // FIXME: Add this for backwards compatibility, should be fixed at some point probably if($logger === null) { - // TODO: Drop backwards compatibility for config in the future $logType = $this->config->getValue('log_type', 'file'); - if($logType==='owncloud') { - $logType = 'file'; - } - $this->logger = 'OC\\Log\\'.ucfirst($logType); + $this->logger = static::getLogClass($logType); call_user_func(array($this->logger, 'init')); } else { $this->logger = $logger; @@ -327,4 +323,26 @@ class Log implements ILogger { $msg .= ': ' . json_encode($exception); $this->error($msg, $context); } + + /** + * @param string $logType + * @return string + * @internal + */ + public static function getLogClass($logType) { + switch (strtolower($logType)) { + case 'errorlog': + return \OC\Log\Errorlog::class; + case 'syslog': + return \OC\Log\Syslog::class; + case 'file': + return \OC\Log\File::class; + + // Backwards compatibility for old and fallback for unknown log types + case 'owncloud': + case 'nextcloud': + default: + return \OC\Log\File::class; + } + } } diff --git a/lib/private/Log/File.php b/lib/private/Log/File.php index 904aa1d93f1..be8b72b3a3f 100644 --- a/lib/private/Log/File.php +++ b/lib/private/Log/File.php @@ -75,8 +75,8 @@ class File { $config = \OC::$server->getSystemConfig(); // default to ISO8601 - $format = $config->getValue('logdateformat', 'c'); - $logTimeZone = $config->getValue( "logtimezone", 'UTC' ); + $format = $config->getValue('logdateformat', \DateTime::ATOM); + $logTimeZone = $config->getValue('logtimezone', 'UTC'); try { $timezone = new \DateTimeZone($logTimeZone); } catch (\Exception $e) { diff --git a/lib/private/Log/Rotate.php b/lib/private/Log/Rotate.php index 6c87c167378..866068433ff 100644 --- a/lib/private/Log/Rotate.php +++ b/lib/private/Log/Rotate.php @@ -32,7 +32,9 @@ namespace OC\Log; */ class Rotate extends \OC\BackgroundJob\Job { private $max_log_size; - public function run($logFile) { + public function run($dummy) { + $systemConfig = \OC::$server->getSystemConfig(); + $logFile = $systemConfig->getValue('logfile', $systemConfig->getValue('datadirectory', \OC::$SERVERROOT . '/data') . '/nextcloud.log'); $this->max_log_size = \OC::$server->getConfig()->getSystemValue('log_rotate_size', false); if ($this->max_log_size) { $filesize = @filesize($logFile); diff --git a/lib/private/Mail/Mailer.php b/lib/private/Mail/Mailer.php index df8fa80f2c2..e704e8e3490 100644 --- a/lib/private/Mail/Mailer.php +++ b/lib/private/Mail/Mailer.php @@ -200,7 +200,7 @@ class Mailer implements IMailer { * @return \Swift_SendmailTransport */ protected function getSendMailInstance() { - switch ($this->config->getSystemValue('mail_smtpmode', 'sendmail')) { + switch ($this->config->getSystemValue('mail_smtpmode', 'php')) { case 'qmail': $binaryPath = '/var/qmail/bin/sendmail'; break; diff --git a/lib/private/Memcache/APC.php b/lib/private/Memcache/APC.php deleted file mode 100644 index 7db6d64b085..00000000000 --- a/lib/private/Memcache/APC.php +++ /dev/null @@ -1,136 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Andreas Fischer <bantu@owncloud.com> - * @author Clark Tomlinson <fallen013@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Otto Sabart <ottosabart@seberm.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Memcache; - -use OCP\IMemcache; - -class APC extends Cache implements IMemcache { - use CASTrait { - cas as casEmulated; - } - - use CADTrait; - - public function get($key) { - $result = apc_fetch($this->getPrefix() . $key, $success); - if (!$success) { - return null; - } - return $result; - } - - public function set($key, $value, $ttl = 0) { - return apc_store($this->getPrefix() . $key, $value, $ttl); - } - - public function hasKey($key) { - return apc_exists($this->getPrefix() . $key); - } - - public function remove($key) { - return apc_delete($this->getPrefix() . $key); - } - - public function clear($prefix = '') { - $ns = $this->getPrefix() . $prefix; - $ns = preg_quote($ns, '/'); - $iter = new \APCIterator('user', '/^' . $ns . '/', APC_ITER_KEY); - return apc_delete($iter); - } - - /** - * Set a value in the cache if it's not already stored - * - * @param string $key - * @param mixed $value - * @param int $ttl Time To Live in seconds. Defaults to 60*60*24 - * @return bool - */ - public function add($key, $value, $ttl = 0) { - return apc_add($this->getPrefix() . $key, $value, $ttl); - } - - /** - * Increase a stored number - * - * @param string $key - * @param int $step - * @return int | bool - */ - public function inc($key, $step = 1) { - $this->add($key, 0); - return apc_inc($this->getPrefix() . $key, $step); - } - - /** - * Decrease a stored number - * - * @param string $key - * @param int $step - * @return int | bool - */ - public function dec($key, $step = 1) { - return apc_dec($this->getPrefix() . $key, $step); - } - - /** - * Compare and set - * - * @param string $key - * @param mixed $old - * @param mixed $new - * @return bool - */ - public function cas($key, $old, $new) { - // apc only does cas for ints - if (is_int($old) and is_int($new)) { - return apc_cas($this->getPrefix() . $key, $old, $new); - } else { - return $this->casEmulated($key, $old, $new); - } - } - - static public function isAvailable() { - if (!extension_loaded('apc')) { - return false; - } elseif (!\OC::$server->getIniWrapper()->getBool('apc.enabled')) { - return false; - } elseif (!\OC::$server->getIniWrapper()->getBool('apc.enable_cli') && \OC::$CLI) { - return false; - } else { - return true; - } - } -} - -if (!function_exists('apc_exists')) { - function apc_exists($keys) { - $result = false; - apc_fetch($keys, $result); - return $result; - } -} diff --git a/lib/private/Memcache/Factory.php b/lib/private/Memcache/Factory.php index f3841d31679..8e62e020faa 100644 --- a/lib/private/Memcache/Factory.php +++ b/lib/private/Memcache/Factory.php @@ -83,7 +83,7 @@ class Factory implements ICacheFactory { $missingCacheMessage = 'Memcache {class} not available for {use} cache'; $missingCacheHint = 'Is the matching PHP module installed and enabled?'; - if (!$localCacheClass::isAvailable()) { + if (!class_exists($localCacheClass) || !$localCacheClass::isAvailable()) { if (\OC::$CLI && !defined('PHPUNIT_RUN')) { // CLI should not hard-fail on broken memcache $this->logger->info($missingCacheMessage, [ @@ -98,7 +98,7 @@ class Factory implements ICacheFactory { ]), $missingCacheHint); } } - if (!$distributedCacheClass::isAvailable()) { + if (!class_exists($distributedCacheClass) || !$distributedCacheClass::isAvailable()) { if (\OC::$CLI && !defined('PHPUNIT_RUN')) { // CLI should not hard-fail on broken memcache $this->logger->info($missingCacheMessage, [ @@ -113,7 +113,7 @@ class Factory implements ICacheFactory { ]), $missingCacheHint); } } - if (!($lockingCacheClass && $lockingCacheClass::isAvailable())) { + if (!($lockingCacheClass && class_exists($distributedCacheClass) && $lockingCacheClass::isAvailable())) { // don't fallback since the fallback might not be suitable for storing lock $lockingCacheClass = self::NULL_CACHE; } diff --git a/lib/private/Memcache/Memcached.php b/lib/private/Memcache/Memcached.php index 12f45739374..bf07fd0e6e7 100644 --- a/lib/private/Memcache/Memcached.php +++ b/lib/private/Memcache/Memcached.php @@ -46,16 +46,6 @@ class Memcached extends Cache implements IMemcache { parent::__construct($prefix); if (is_null(self::$cache)) { self::$cache = new \Memcached(); - $servers = \OC::$server->getSystemConfig()->getValue('memcached_servers'); - if (!$servers) { - $server = \OC::$server->getSystemConfig()->getValue('memcached_server'); - if ($server) { - $servers = array($server); - } else { - $servers = array(array('localhost', 11211)); - } - } - self::$cache->addServers($servers); $defaultOptions = [ \Memcached::OPT_CONNECT_TIMEOUT => 50, @@ -71,7 +61,7 @@ class Memcached extends Cache implements IMemcache { \Memcached::OPT_LIBKETAMA_COMPATIBLE => true, // Enable Binary Protocol - \Memcached::OPT_BINARY_PROTOCOL => true, + //\Memcached::OPT_BINARY_PROTOCOL => true, ]; // by default enable igbinary serializer if available if (\Memcached::HAVE_IGBINARY) { @@ -85,6 +75,17 @@ class Memcached extends Cache implements IMemcache { } else { throw new HintException("Expected 'memcached_options' config to be an array, got $options"); } + + $servers = \OC::$server->getSystemConfig()->getValue('memcached_servers'); + if (!$servers) { + $server = \OC::$server->getSystemConfig()->getValue('memcached_server'); + if ($server) { + $servers = [$server]; + } else { + $servers = [['localhost', 11211]]; + } + } + self::$cache->addServers($servers); } } @@ -110,7 +111,9 @@ class Memcached extends Cache implements IMemcache { } else { $result = self::$cache->set($this->getNamespace() . $key, $value); } - $this->verifyReturnCode(); + if ($result !== true) { + $this->verifyReturnCode(); + } return $result; } diff --git a/lib/private/NavigationManager.php b/lib/private/NavigationManager.php index 3e9ddfc6466..f7bc02943a3 100644 --- a/lib/private/NavigationManager.php +++ b/lib/private/NavigationManager.php @@ -1,6 +1,6 @@ <?php /** - * @copyright Copyright (c) 2016, ownCloud, Inc. + * @copyright Copyright (c) 2016, ownCloud GmbH * * @author Bart Visscher <bartv@thisnet.nl> * @author Joas Schilling <coding@schilljs.com> @@ -26,13 +26,46 @@ namespace OC; +use OC\App\AppManager; +use OCP\App\IAppManager; +use OCP\IGroupManager; +use OCP\INavigationManager; +use OCP\IURLGenerator; +use OCP\IUserSession; +use OCP\L10N\IFactory; + /** * Manages the ownCloud navigation */ -class NavigationManager implements \OCP\INavigationManager { - protected $entries = array(); - protected $closureEntries = array(); + +class NavigationManager implements INavigationManager { + protected $entries = []; + protected $closureEntries = []; protected $activeEntry; + /** @var bool */ + protected $init = false; + /** @var IAppManager|AppManager */ + protected $appManager; + /** @var IURLGenerator */ + private $urlGenerator; + /** @var IFactory */ + private $l10nFac; + /** @var IUserSession */ + private $userSession; + /** @var IGroupManager */ + private $groupManager; + + public function __construct(IAppManager $appManager = null, + IURLGenerator $urlGenerator = null, + IFactory $l10nFac = null, + IUserSession $userSession = null, + IGroupManager$groupManager = null) { + $this->appManager = $appManager; + $this->urlGenerator = $urlGenerator; + $this->l10nFac = $l10nFac; + $this->userSession = $userSession; + $this->groupManager = $groupManager; + } /** * Creates a new navigation entry @@ -60,6 +93,7 @@ class NavigationManager implements \OCP\INavigationManager { * @return array an array of the added entries */ public function getAll() { + $this->init(); foreach ($this->closureEntries as $c) { $this->add($c()); } @@ -71,8 +105,9 @@ class NavigationManager implements \OCP\INavigationManager { * removes all the entries */ public function clear() { - $this->entries = array(); - $this->closureEntries = array(); + $this->entries = []; + $this->closureEntries = []; + $this->init = false; } /** @@ -93,4 +128,64 @@ class NavigationManager implements \OCP\INavigationManager { public function getActiveEntry() { return $this->activeEntry; } + + private function init() { + if ($this->init) { + return; + } + $this->init = true; + if (is_null($this->appManager)) { + return; + } + foreach ($this->appManager->getInstalledApps() as $app) { + // load plugins and collections from info.xml + $info = $this->appManager->getAppInfo($app); + if (!isset($info['navigation'])) { + continue; + } + $nav = $info['navigation']; + if (!isset($nav['name'])) { + continue; + } + if (!isset($nav['route'])) { + continue; + } + $role = isset($nav['@attributes']['role']) ? $nav['@attributes']['role'] : 'all'; + if ($role === 'admin' && !$this->isAdmin()) { + continue; + } + $l = $this->l10nFac->get($app); + $order = isset($nav['order']) ? $nav['order'] : 100; + $route = $this->urlGenerator->linkToRoute($nav['route']); + $icon = isset($nav['icon']) ? $nav['icon'] : 'app.svg'; + foreach ([$icon, "$app.svg"] as $i) { + try { + $icon = $this->urlGenerator->imagePath($app, $i); + break; + } catch (\RuntimeException $ex) { + // no icon? - ignore it then + } + } + if (is_null($icon)) { + $icon = $this->urlGenerator->imagePath('core', 'default-app-icon'); + } + + $this->add([ + 'id' => $app, + 'order' => $order, + 'href' => $route, + 'icon' => $icon, + 'name' => $l->t($nav['name']), + ]); + } + } + + private function isAdmin() { + $user = $this->userSession->getUser(); + if ($user !== null) { + return $this->groupManager->isAdmin($user->getUID()); + } + return false; + } + } diff --git a/lib/private/Preview.php b/lib/private/Preview.php deleted file mode 100644 index caa1e89bacc..00000000000 --- a/lib/private/Preview.php +++ /dev/null @@ -1,1349 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Björn Schießle <bjoern@schiessle.org> - * @author Frank Karlitschek <frank@karlitschek.de> - * @author Georg Ehrke <georg@owncloud.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Olivier Paroz <github@oparoz.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Tobias Kaminsky <tobias@kaminsky.me> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OC; - -use OC\Preview\Provider; -use OCP\Files\FileInfo; -use OCP\Files\NotFoundException; - -class Preview { - //the thumbnail folder - const THUMBNAILS_FOLDER = 'thumbnails'; - - const MODE_FILL = 'fill'; - const MODE_COVER = 'cover'; - - //config - private $maxScaleFactor; - /** @var int maximum width allowed for a preview */ - private $configMaxWidth; - /** @var int maximum height allowed for a preview */ - private $configMaxHeight; - - //fileview object - private $fileView = null; - private $userView = null; - - //vars - private $file; - private $maxX; - private $maxY; - private $scalingUp; - private $mimeType; - private $keepAspect = false; - private $mode = self::MODE_FILL; - - //used to calculate the size of the preview to generate - /** @var int $maxPreviewWidth max width a preview can have */ - private $maxPreviewWidth; - /** @var int $maxPreviewHeight max height a preview can have */ - private $maxPreviewHeight; - /** @var int $previewWidth calculated width of the preview we're looking for */ - private $previewWidth; - /** @var int $previewHeight calculated height of the preview we're looking for */ - private $previewHeight; - - //filemapper used for deleting previews - // index is path, value is fileinfo - static public $deleteFileMapper = array(); - static public $deleteChildrenMapper = array(); - - /** - * preview images object - * - * @var \OCP\IImage - */ - private $preview; - - /** - * @var \OCP\Files\FileInfo - */ - protected $info; - - /** - * check if thumbnail or bigger version of thumbnail of file is cached - * - * @param string $user userid - if no user is given, OC_User::getUser will be used - * @param string $root path of root - * @param string $file The path to the file where you want a thumbnail from - * @param int $maxX The maximum X size of the thumbnail. It can be smaller depending on the - * shape of the image - * @param int $maxY The maximum Y size of the thumbnail. It can be smaller depending on the - * shape of the image - * @param bool $scalingUp Disable/Enable upscaling of previews - * - * @throws \Exception - * @return mixed (bool / string) - * false if thumbnail does not exist - * path to thumbnail if thumbnail exists - */ - public function __construct( - $user = '', - $root = '/', - $file = '', $maxX = 1, - $maxY = 1, - $scalingUp = true - ) { - //init fileviews - if ($user === '') { - $user = \OC_User::getUser(); - } - $this->fileView = new \OC\Files\View('/' . $user . '/' . $root); - $this->userView = new \OC\Files\View('/' . $user); - - //set config - $sysConfig = \OC::$server->getConfig(); - $this->configMaxWidth = $sysConfig->getSystemValue('preview_max_x', 2048); - $this->configMaxHeight = $sysConfig->getSystemValue('preview_max_y', 2048); - $this->maxScaleFactor = $sysConfig->getSystemValue('preview_max_scale_factor', 1); - - //save parameters - $this->setFile($file); - $this->setMaxX((int)$maxX); - $this->setMaxY((int)$maxY); - $this->setScalingup($scalingUp); - - $this->preview = null; - - //check if there are preview backends - if (!\OC::$server->getPreviewManager() - ->hasProviders() - && \OC::$server->getConfig() - ->getSystemValue('enable_previews', true) - ) { - \OCP\Util::writeLog('core', 'No preview providers exist', \OCP\Util::ERROR); - throw new \Exception('No preview providers'); - } - } - - /** - * returns the path of the file you want a thumbnail from - * - * @return string - */ - public function getFile() { - return $this->file; - } - - /** - * returns the max width of the preview - * - * @return integer - */ - public function getMaxX() { - return $this->maxX; - } - - /** - * returns the max height of the preview - * - * @return integer - */ - public function getMaxY() { - return $this->maxY; - } - - /** - * returns whether or not scalingup is enabled - * - * @return bool - */ - public function getScalingUp() { - return $this->scalingUp; - } - - /** - * returns the name of the thumbnailfolder - * - * @return string - */ - public function getThumbnailsFolder() { - return self::THUMBNAILS_FOLDER; - } - - /** - * returns the max scale factor - * - * @return string - */ - public function getMaxScaleFactor() { - return $this->maxScaleFactor; - } - - /** - * returns the max width set in ownCloud's config - * - * @return integer - */ - public function getConfigMaxX() { - return $this->configMaxWidth; - } - - /** - * returns the max height set in ownCloud's config - * - * @return integer - */ - public function getConfigMaxY() { - return $this->configMaxHeight; - } - - /** - * Returns the FileInfo object associated with the file to preview - * - * @return false|Files\FileInfo|\OCP\Files\FileInfo - */ - protected function getFileInfo() { - $absPath = $this->fileView->getAbsolutePath($this->file); - $absPath = Files\Filesystem::normalizePath($absPath); - if (array_key_exists($absPath, self::$deleteFileMapper)) { - $this->info = self::$deleteFileMapper[$absPath]; - } else if (!$this->info) { - $this->info = $this->fileView->getFileInfo($this->file); - } - - return $this->info; - } - - - /** - * @return array|null - */ - private function getChildren() { - $absPath = $this->fileView->getAbsolutePath($this->file); - $absPath = Files\Filesystem::normalizePath($absPath); - - if (array_key_exists($absPath, self::$deleteChildrenMapper)) { - return self::$deleteChildrenMapper[$absPath]; - } - - return null; - } - - /** - * Sets the path of the file you want a preview of - * - * @param string $file - * @param \OCP\Files\FileInfo|null $info - * - * @return \OC\Preview - */ - public function setFile($file, $info = null) { - $this->file = $file; - $this->info = $info; - - if ($file !== '') { - $this->getFileInfo(); - if ($this->info instanceof \OCP\Files\FileInfo) { - $this->mimeType = $this->info->getMimetype(); - } - } - - return $this; - } - - /** - * Forces the use of a specific media type - * - * @param string $mimeType - */ - public function setMimetype($mimeType) { - $this->mimeType = $mimeType; - } - - /** - * Sets the max width of the preview. It's capped by the maximum allowed size set in the - * configuration - * - * @param int $maxX - * - * @throws \Exception - * @return \OC\Preview - */ - public function setMaxX($maxX = 1) { - if ($maxX <= 0) { - throw new \Exception('Cannot set width of 0 or smaller!'); - } - $configMaxX = $this->getConfigMaxX(); - $maxX = $this->limitMaxDim($maxX, $configMaxX, 'maxX'); - $this->maxX = $maxX; - - return $this; - } - - /** - * Sets the max height of the preview. It's capped by the maximum allowed size set in the - * configuration - * - * @param int $maxY - * - * @throws \Exception - * @return \OC\Preview - */ - public function setMaxY($maxY = 1) { - if ($maxY <= 0) { - throw new \Exception('Cannot set height of 0 or smaller!'); - } - $configMaxY = $this->getConfigMaxY(); - $maxY = $this->limitMaxDim($maxY, $configMaxY, 'maxY'); - $this->maxY = $maxY; - - return $this; - } - - /** - * Sets whether we're allowed to scale up when generating a preview. It's capped by the maximum - * allowed scale factor set in the configuration - * - * @param bool $scalingUp - * - * @return \OC\Preview - */ - public function setScalingup($scalingUp) { - if ($this->getMaxScaleFactor() === 1) { - $scalingUp = false; - } - $this->scalingUp = $scalingUp; - - return $this; - } - - /** - * Set whether to cover or fill the specified dimensions - * - * @param string $mode - * - * @return \OC\Preview - */ - public function setMode($mode) { - $this->mode = $mode; - - return $this; - } - - /** - * Sets whether we need to generate a preview which keeps the aspect ratio of the original file - * - * @param bool $keepAspect - * - * @return \OC\Preview - */ - public function setKeepAspect($keepAspect) { - $this->keepAspect = $keepAspect; - - return $this; - } - - /** - * Makes sure we were given a file to preview and that it exists in the filesystem - * - * @return bool - */ - public function isFileValid() { - $file = $this->getFile(); - if ($file === '') { - \OCP\Util::writeLog('core', 'No filename passed', \OCP\Util::DEBUG); - - return false; - } - - if (!$this->getFileInfo() instanceof FileInfo) { - \OCP\Util::writeLog('core', 'File:"' . $file . '" not found', \OCP\Util::DEBUG); - - return false; - } - - return true; - } - - /** - * Deletes the preview of a file with specific width and height - * - * This should never delete the max preview, use deleteAllPreviews() instead - * - * @return bool - */ - public function deletePreview() { - $fileInfo = $this->getFileInfo(); - if ($fileInfo !== null && $fileInfo !== false) { - $fileId = $fileInfo->getId(); - - $previewPath = $this->buildCachePath($fileId); - if (!strpos($previewPath, 'max')) { - return $this->userView->unlink($previewPath); - } - } - - return false; - } - - /** - * Deletes all previews of a file - */ - public function deleteAllPreviews() { - $thumbnailMount = $this->userView->getMount($this->getThumbnailsFolder()); - $propagator = $thumbnailMount->getStorage()->getPropagator(); - $propagator->beginBatch(); - - $toDelete = $this->getChildren(); - $toDelete[] = $this->getFileInfo(); - - foreach ($toDelete as $delete) { - if ($delete instanceof FileInfo) { - /** @var \OCP\Files\FileInfo $delete */ - $fileId = $delete->getId(); - - // getId() might return null, e.g. when the file is a - // .ocTransferId*.part file from chunked file upload. - if (!empty($fileId)) { - $previewPath = $this->getPreviewPath($fileId); - $this->userView->rmdir($previewPath); - } - } - } - - $propagator->commitBatch(); - } - - /** - * Checks if a preview matching the asked dimensions or a bigger version is already cached - * - * * We first retrieve the size of the max preview since this is what we be used to create - * all our preview. If it doesn't exist we return false, so that it can be generated - * * Using the dimensions of the max preview, we calculate what the size of the new - * thumbnail should be - * * And finally, we look for a suitable candidate in the cache - * - * @param int $fileId fileId of the original file we need a preview of - * - * @return string|false path to the cached preview if it exists or false - */ - public function isCached($fileId) { - if (is_null($fileId)) { - return false; - } - - /** - * Phase 1: Looking for the max preview - */ - $previewPath = $this->getPreviewPath($fileId); - // We currently can't look for a single file due to bugs related to #16478 - $allThumbnails = $this->userView->getDirectoryContent($previewPath); - list($maxPreviewWidth, $maxPreviewHeight) = $this->getMaxPreviewSize($allThumbnails); - - // Only use the cache if we have a max preview - if (!is_null($maxPreviewWidth) && !is_null($maxPreviewHeight)) { - - /** - * Phase 2: Calculating the size of the preview we need to send back - */ - $this->maxPreviewWidth = $maxPreviewWidth; - $this->maxPreviewHeight = $maxPreviewHeight; - - list($previewWidth, $previewHeight) = $this->simulatePreviewDimensions(); - if (empty($previewWidth) || empty($previewHeight)) { - return false; - } - - $this->previewWidth = $previewWidth; - $this->previewHeight = $previewHeight; - - /** - * Phase 3: We look for a preview of the exact size - */ - // This gives us a calculated path to a preview of asked dimensions - // thumbnailFolder/fileId/<maxX>-<maxY>(-max|-with-aspect).png - $preview = $this->buildCachePath($fileId, $previewWidth, $previewHeight); - - // This checks if we have a preview of those exact dimensions in the cache - if ($this->thumbnailSizeExists($allThumbnails, basename($preview))) { - return $preview; - } - - /** - * Phase 4: We look for a larger preview, matching the aspect ratio - */ - if (($this->getMaxX() >= $maxPreviewWidth) - && ($this->getMaxY() >= $maxPreviewHeight) - ) { - // The preview we-re looking for is the exact size or larger than the max preview, - // so return that - return $this->buildCachePath($fileId, $maxPreviewWidth, $maxPreviewHeight); - } else { - // The last resort is to look for something bigger than what we've calculated, - // but still smaller than the max preview - return $this->isCachedBigger($fileId, $allThumbnails); - } - } - - return false; - } - - /** - * Returns the dimensions of the max preview - * - * @param FileInfo[] $allThumbnails the list of all our cached thumbnails - * - * @return int[] - */ - private function getMaxPreviewSize($allThumbnails) { - $maxPreviewX = null; - $maxPreviewY = null; - - foreach ($allThumbnails as $thumbnail) { - $name = $thumbnail['name']; - if (strpos($name, 'max')) { - list($maxPreviewX, $maxPreviewY) = $this->getDimensionsFromFilename($name); - break; - } - } - - return [$maxPreviewX, $maxPreviewY]; - } - - /** - * Check if a specific thumbnail size is cached - * - * @param FileInfo[] $allThumbnails the list of all our cached thumbnails - * @param string $name - * @return bool - */ - private function thumbnailSizeExists(array $allThumbnails, $name) { - - foreach ($allThumbnails as $thumbnail) { - if ($name === $thumbnail->getName()) { - return true; - } - } - - return false; - } - - /** - * Determines the size of the preview we should be looking for in the cache - * - * @return integer[] - */ - private function simulatePreviewDimensions() { - $askedWidth = $this->getMaxX(); - $askedHeight = $this->getMaxY(); - - if ($this->keepAspect) { - list($newPreviewWidth, $newPreviewHeight) = - $this->applyAspectRatio($askedWidth, $askedHeight); - } else { - list($newPreviewWidth, $newPreviewHeight) = $this->fixSize($askedWidth, $askedHeight); - } - - return [(int)$newPreviewWidth, (int)$newPreviewHeight]; - } - - /** - * Resizes the boundaries to match the aspect ratio - * - * @param int $askedWidth - * @param int $askedHeight - * - * @param int $originalWidth - * @param int $originalHeight - * @return integer[] - */ - private function applyAspectRatio($askedWidth, $askedHeight, $originalWidth = 0, $originalHeight = 0) { - if (!$originalWidth) { - $originalWidth = $this->maxPreviewWidth; - } - if (!$originalHeight) { - $originalHeight = $this->maxPreviewHeight; - } - $originalRatio = $originalWidth / $originalHeight; - // Defines the box in which the preview has to fit - $scaleFactor = $this->scalingUp ? $this->maxScaleFactor : 1; - $askedWidth = min($askedWidth, $originalWidth * $scaleFactor); - $askedHeight = min($askedHeight, $originalHeight * $scaleFactor); - - if ($askedWidth / $originalRatio < $askedHeight) { - // width restricted - $askedHeight = round($askedWidth / $originalRatio); - } else { - $askedWidth = round($askedHeight * $originalRatio); - } - - return [(int)$askedWidth, (int)$askedHeight]; - } - - /** - * Resizes the boundaries to cover the area - * - * @param int $askedWidth - * @param int $askedHeight - * @param int $previewWidth - * @param int $previewHeight - * @return integer[] - */ - private function applyCover($askedWidth, $askedHeight, $previewWidth, $previewHeight) { - $originalRatio = $previewWidth / $previewHeight; - // Defines the box in which the preview has to fit - $scaleFactor = $this->scalingUp ? $this->maxScaleFactor : 1; - $askedWidth = min($askedWidth, $previewWidth * $scaleFactor); - $askedHeight = min($askedHeight, $previewHeight * $scaleFactor); - - if ($askedWidth / $originalRatio > $askedHeight) { - // height restricted - $askedHeight = round($askedWidth / $originalRatio); - } else { - $askedWidth = round($askedHeight * $originalRatio); - } - - return [(int)$askedWidth, (int)$askedHeight]; - } - - /** - * Makes sure an upscaled preview doesn't end up larger than the max dimensions defined in the - * config - * - * @param int $askedWidth - * @param int $askedHeight - * - * @return integer[] - */ - private function fixSize($askedWidth, $askedHeight) { - if ($this->scalingUp) { - $askedWidth = min($this->configMaxWidth, $askedWidth); - $askedHeight = min($this->configMaxHeight, $askedHeight); - } - - return [(int)$askedWidth, (int)$askedHeight]; - } - - /** - * Checks if a bigger version of a file preview is cached and if not - * return the preview of max allowed dimensions - * - * @param int $fileId fileId of the original image - * @param FileInfo[] $allThumbnails the list of all our cached thumbnails - * - * @return string path to bigger thumbnail - */ - private function isCachedBigger($fileId, $allThumbnails) { - // This is used to eliminate any thumbnail narrower than what we need - $maxX = $this->getMaxX(); - - //array for usable cached thumbnails - $possibleThumbnails = $this->getPossibleThumbnails($allThumbnails); - - foreach ($possibleThumbnails as $width => $path) { - if ($width < $maxX) { - continue; - } else { - return $path; - } - } - - // At this stage, we didn't find a preview, so we return the max preview - return $this->buildCachePath($fileId, $this->maxPreviewWidth, $this->maxPreviewHeight); - } - - /** - * Get possible bigger thumbnails of the given image with the proper aspect ratio - * - * @param FileInfo[] $allThumbnails the list of all our cached thumbnails - * - * @return string[] an array of paths to bigger thumbnails - */ - private function getPossibleThumbnails($allThumbnails) { - if ($this->keepAspect) { - $wantedAspectRatio = (float)($this->maxPreviewWidth / $this->maxPreviewHeight); - } else { - $wantedAspectRatio = (float)($this->getMaxX() / $this->getMaxY()); - } - - //array for usable cached thumbnails - $possibleThumbnails = array(); - foreach ($allThumbnails as $thumbnail) { - $name = rtrim($thumbnail['name'], '.png'); - list($x, $y, $aspectRatio) = $this->getDimensionsFromFilename($name); - if (abs($aspectRatio - $wantedAspectRatio) >= 0.000001 - || $this->unscalable($x, $y) - ) { - continue; - } - $possibleThumbnails[$x] = $thumbnail['path']; - } - - ksort($possibleThumbnails); - - return $possibleThumbnails; - } - - /** - * Looks at the preview filename from the cache and extracts the size of the preview - * - * @param string $name - * - * @return array<int,int,float> - */ - private function getDimensionsFromFilename($name) { - $size = explode('-', $name); - $x = (int)$size[0]; - $y = (int)$size[1]; - $aspectRatio = (float)($x / $y); - - return array($x, $y, $aspectRatio); - } - - /** - * @param int $x - * @param int $y - * - * @return bool - */ - private function unscalable($x, $y) { - - $maxX = $this->getMaxX(); - $maxY = $this->getMaxY(); - $scalingUp = $this->getScalingUp(); - $maxScaleFactor = $this->getMaxScaleFactor(); - - if ($x < $maxX || $y < $maxY) { - if ($scalingUp) { - $scaleFactor = $maxX / $x; - if ($scaleFactor > $maxScaleFactor) { - return true; - } - } else { - return true; - } - } - - return false; - } - - /** - * Returns a preview of a file - * - * The cache is searched first and if nothing usable was found then a preview is - * generated by one of the providers - * - * @return \OCP\IImage - */ - public function getPreview() { - if (!is_null($this->preview) && $this->preview->valid()) { - return $this->preview; - } - - $this->preview = null; - $fileInfo = $this->getFileInfo(); - if ($fileInfo === null || $fileInfo === false || !$fileInfo->isReadable()) { - return new \OC_Image(); - } - - $fileId = $fileInfo->getId(); - $cached = $this->isCached($fileId); - if ($cached) { - $this->getCachedPreview($fileId, $cached); - } - - if (is_null($this->preview)) { - $this->generatePreview($fileId); - } - - // We still don't have a preview, so we send back an empty object - if (is_null($this->preview)) { - $this->preview = new \OC_Image(); - } - - return $this->preview; - } - - /** - * Sends the preview, including the headers to client which requested it - * - * @param null|string $mimeTypeForHeaders the media type to use when sending back the reply - * - * @throws NotFoundException - * @throws PreviewNotAvailableException - */ - public function showPreview($mimeTypeForHeaders = null) { - // Check if file is valid - if ($this->isFileValid() === false) { - throw new NotFoundException('File not found.'); - } - - if (is_null($this->preview)) { - $this->getPreview(); - } - if ($this->preview instanceof \OCP\IImage) { - if ($this->preview->valid()) { - \OCP\Response::enableCaching(3600 * 24); // 24 hours - } else { - $this->getMimeIcon(); - } - $this->preview->show($mimeTypeForHeaders); - } - } - - /** - * Retrieves the preview from the cache and resizes it if necessary - * - * @param int $fileId fileId of the original image - * @param string $cached the path to the cached preview - */ - private function getCachedPreview($fileId, $cached) { - $stream = $this->userView->fopen($cached, 'r'); - $this->preview = null; - if ($stream) { - $image = new \OC_Image(); - $image->loadFromFileHandle($stream); - - $this->preview = $image->valid() ? $image : null; - - if (!is_null($this->preview)) { - // Size of the preview we calculated - $maxX = $this->previewWidth; - $maxY = $this->previewHeight; - // Size of the preview we retrieved from the cache - $previewX = (int)$this->preview->width(); - $previewY = (int)$this->preview->height(); - - // We don't have an exact match - if ($previewX !== $maxX || $previewY !== $maxY) { - $this->resizeAndStore($fileId); - } - } - - fclose($stream); - } - } - - /** - * Resizes, crops, fixes orientation and stores in the cache - * - * @param int $fileId fileId of the original image - */ - private function resizeAndStore($fileId) { - $image = $this->preview; - if (!($image instanceof \OCP\IImage)) { - \OCP\Util::writeLog( - 'core', '$this->preview is not an instance of \OCP\IImage', \OCP\Util::DEBUG - ); - - return; - } - $previewWidth = (int)$image->width(); - $previewHeight = (int)$image->height(); - $askedWidth = $this->getMaxX(); - $askedHeight = $this->getMaxY(); - - if ($this->mode === self::MODE_COVER) { - list($askedWidth, $askedHeight) = - $this->applyCover($askedWidth, $askedHeight, $previewWidth, $previewHeight); - } - - /** - * Phase 1: If required, adjust boundaries to keep aspect ratio - */ - if ($this->keepAspect) { - list($askedWidth, $askedHeight) = - $this->applyAspectRatio($askedWidth, $askedHeight, $previewWidth, $previewHeight); - } - - /** - * Phase 2: Resizes preview to try and match requirements. - * Takes the scaling ratio into consideration - */ - list($newPreviewWidth, $newPreviewHeight) = $this->scale( - $image, $askedWidth, $askedHeight, $previewWidth, $previewHeight - ); - - // The preview has been resized and should now have the asked dimensions - if ($newPreviewWidth === $askedWidth && $newPreviewHeight === $askedHeight) { - $this->storePreview($fileId, $newPreviewWidth, $newPreviewHeight); - - return; - } - - /** - * Phase 3: We're still not there yet, so we're clipping and filling - * to match the asked dimensions - */ - // It turns out the scaled preview is now too big, so we crop the image - if ($newPreviewWidth >= $askedWidth && $newPreviewHeight >= $askedHeight) { - $this->crop($image, $askedWidth, $askedHeight, $newPreviewWidth, $newPreviewHeight); - $this->storePreview($fileId, $askedWidth, $askedHeight); - - return; - } - - // At least one dimension of the scaled preview is too small, - // so we fill the space with a transparent background - if (($newPreviewWidth < $askedWidth || $newPreviewHeight < $askedHeight)) { - $this->cropAndFill( - $image, $askedWidth, $askedHeight, $newPreviewWidth, $newPreviewHeight - ); - $this->storePreview($fileId, $askedWidth, $askedHeight); - - return; - } - - // The preview is smaller, but we can't touch it - $this->storePreview($fileId, $newPreviewWidth, $newPreviewHeight); - } - - /** - * Calculates the new dimensions of the preview - * - * The new dimensions can be larger or smaller than the ones of the preview we have to resize - * - * @param \OCP\IImage $image - * @param int $askedWidth - * @param int $askedHeight - * @param int $previewWidth - * @param int $previewHeight - * - * @return int[] - */ - private function scale($image, $askedWidth, $askedHeight, $previewWidth, $previewHeight) { - $scalingUp = $this->getScalingUp(); - $maxScaleFactor = $this->getMaxScaleFactor(); - - $factorX = $askedWidth / $previewWidth; - $factorY = $askedHeight / $previewHeight; - - if ($factorX >= $factorY) { - $factor = $factorX; - } else { - $factor = $factorY; - } - - if ($scalingUp === false) { - if ($factor > 1) { - $factor = 1; - } - } - - // We cap when upscaling - if (!is_null($maxScaleFactor)) { - if ($factor > $maxScaleFactor) { - \OCP\Util::writeLog( - 'core', 'scale factor reduced from ' . $factor . ' to ' . $maxScaleFactor, - \OCP\Util::DEBUG - ); - $factor = $maxScaleFactor; - } - } - - $newPreviewWidth = round($previewWidth * $factor); - $newPreviewHeight = round($previewHeight * $factor); - - $image->preciseResize($newPreviewWidth, $newPreviewHeight); - $this->preview = $image; - - return [$newPreviewWidth, $newPreviewHeight]; - } - - /** - * Crops a preview which is larger than the dimensions we've received - * - * @param \OCP\IImage $image - * @param int $askedWidth - * @param int $askedHeight - * @param int $previewWidth - * @param int $previewHeight - */ - private function crop($image, $askedWidth, $askedHeight, $previewWidth, $previewHeight = null) { - $cropX = floor(abs($askedWidth - $previewWidth) * 0.5); - //don't crop previews on the Y axis, this sucks if it's a document. - //$cropY = floor(abs($y - $newPreviewHeight) * 0.5); - $cropY = 0; - $image->crop($cropX, $cropY, $askedWidth, $askedHeight); - $this->preview = $image; - } - - /** - * Crops an image if it's larger than the dimensions we've received and fills the empty space - * with a transparent background - * - * @param \OCP\IImage $image - * @param int $askedWidth - * @param int $askedHeight - * @param int $previewWidth - * @param int $previewHeight - */ - private function cropAndFill($image, $askedWidth, $askedHeight, $previewWidth, $previewHeight) { - if ($previewWidth > $askedWidth) { - $cropX = floor(($previewWidth - $askedWidth) * 0.5); - $image->crop($cropX, 0, $askedWidth, $previewHeight); - $previewWidth = $askedWidth; - } - - if ($previewHeight > $askedHeight) { - $cropY = floor(($previewHeight - $askedHeight) * 0.5); - $image->crop(0, $cropY, $previewWidth, $askedHeight); - $previewHeight = $askedHeight; - } - - // Creates a transparent background - $backgroundLayer = imagecreatetruecolor($askedWidth, $askedHeight); - imagealphablending($backgroundLayer, false); - $transparency = imagecolorallocatealpha($backgroundLayer, 0, 0, 0, 127); - imagefill($backgroundLayer, 0, 0, $transparency); - imagesavealpha($backgroundLayer, true); - - $image = $image->resource(); - - $mergeX = floor(abs($askedWidth - $previewWidth) * 0.5); - $mergeY = floor(abs($askedHeight - $previewHeight) * 0.5); - - // Pastes the preview on top of the background - imagecopy( - $backgroundLayer, $image, $mergeX, $mergeY, 0, 0, $previewWidth, - $previewHeight - ); - - $image = new \OC_Image($backgroundLayer); - - $this->preview = $image; - } - - /** - * Saves a preview in the cache to speed up future calls - * - * Do not nullify the preview as it might send the whole process in a loop - * - * @param int $fileId fileId of the original image - * @param int $previewWidth - * @param int $previewHeight - */ - private function storePreview($fileId, $previewWidth, $previewHeight) { - if (empty($previewWidth) || empty($previewHeight)) { - \OCP\Util::writeLog( - 'core', 'Cannot save preview of dimension ' . $previewWidth . 'x' . $previewHeight, - \OCP\Util::DEBUG - ); - - } else { - $cachePath = $this->buildCachePath($fileId, $previewWidth, $previewHeight); - $this->userView->file_put_contents($cachePath, $this->preview->data()); - } - } - - /** - * Returns the path to a preview based on its dimensions and aspect - * - * @param int $fileId - * @param int|null $maxX - * @param int|null $maxY - * - * @return string - */ - private function buildCachePath($fileId, $maxX = null, $maxY = null) { - if (is_null($maxX)) { - $maxX = $this->getMaxX(); - } - if (is_null($maxY)) { - $maxY = $this->getMaxY(); - } - - $previewPath = $this->getPreviewPath($fileId); - $previewPath = $previewPath . strval($maxX) . '-' . strval($maxY); - $isMaxPreview = - ($maxX === $this->maxPreviewWidth && $maxY === $this->maxPreviewHeight) ? true : false; - if ($isMaxPreview) { - $previewPath .= '-max'; - } - if ($this->keepAspect && !$isMaxPreview) { - $previewPath .= '-with-aspect'; - } - if ($this->mode === self::MODE_COVER) { - $previewPath .= '-cover'; - } - $previewPath .= '.png'; - - return $previewPath; - } - - /** - * Returns the path to the folder where the previews are stored, identified by the fileId - * - * @param int $fileId - * - * @return string - */ - private function getPreviewPath($fileId) { - return $this->getThumbnailsFolder() . '/' . $fileId . '/'; - } - - /** - * Asks the provider to send a preview of the file which respects the maximum dimensions - * defined in the configuration and after saving it in the cache, it is then resized to the - * asked dimensions - * - * This is only called once in order to generate a large PNG of dimensions defined in the - * configuration file. We'll be able to quickly resize it later on. - * We never upscale the original conversion as this will be done later by the resizing - * operation - * - * @param int $fileId fileId of the original image - */ - private function generatePreview($fileId) { - $file = $this->getFile(); - $preview = null; - - $previewProviders = \OC::$server->getPreviewManager() - ->getProviders(); - foreach ($previewProviders as $supportedMimeType => $providers) { - if (!preg_match($supportedMimeType, $this->mimeType)) { - continue; - } - - foreach ($providers as $closure) { - $provider = $closure(); - if (!($provider instanceof \OCP\Preview\IProvider)) { - continue; - } - - \OCP\Util::writeLog( - 'core', 'Generating preview for "' . $file . '" with "' . get_class($provider) - . '"', \OCP\Util::DEBUG - ); - - /** @var $provider Provider */ - $preview = $provider->getThumbnail( - $file, $this->configMaxWidth, $this->configMaxHeight, $scalingUp = false, - $this->fileView - ); - - if (!($preview instanceof \OCP\IImage)) { - continue; - } - - $this->preview = $preview; - $previewPath = $this->getPreviewPath($fileId); - - if ($this->userView->is_dir($this->getThumbnailsFolder() . '/') === false) { - $this->userView->mkdir($this->getThumbnailsFolder() . '/'); - } - - if ($this->userView->is_dir($previewPath) === false) { - $this->userView->mkdir($previewPath); - } - - // This stores our large preview so that it can be used in subsequent resizing requests - $this->storeMaxPreview($previewPath); - - break 2; - } - } - - // The providers have been kind enough to give us a preview - if ($preview) { - $this->resizeAndStore($fileId); - } - } - - /** - * Defines the media icon, for the media type of the original file, as the preview - * @throws PreviewNotAvailableException - */ - private function getMimeIcon() { - $image = new \OC_Image(); - $mimeIconWebPath = \OC::$server->getMimeTypeDetector()->mimeTypeIcon($this->mimeType); - if (empty(\OC::$WEBROOT)) { - $mimeIconServerPath = \OC::$SERVERROOT . $mimeIconWebPath; - } else { - $mimeIconServerPath = str_replace(\OC::$WEBROOT, \OC::$SERVERROOT, $mimeIconWebPath); - } - // we can't load SVGs into an image - if (substr($mimeIconWebPath, -4) === '.svg') { - throw new PreviewNotAvailableException('SVG mimetype cannot be rendered'); - } - $image->loadFromFile($mimeIconServerPath); - - $this->preview = $image; - } - - /** - * Stores the max preview in the cache - * - * @param string $previewPath path to the preview - */ - private function storeMaxPreview($previewPath) { - $maxPreviewExists = false; - $preview = $this->preview; - - $allThumbnails = $this->userView->getDirectoryContent($previewPath); - // This is so that the cache doesn't need emptying when upgrading - // Can be replaced by an upgrade script... - foreach ($allThumbnails as $thumbnail) { - $name = rtrim($thumbnail['name'], '.png'); - if (strpos($name, 'max')) { - $maxPreviewExists = true; - break; - } - } - // We haven't found the max preview, so we create it - if (!$maxPreviewExists) { - $previewWidth = $preview->width(); - $previewHeight = $preview->height(); - $previewPath = $previewPath . strval($previewWidth) . '-' . strval($previewHeight); - $previewPath .= '-max.png'; - $this->userView->file_put_contents($previewPath, $preview->data()); - $this->maxPreviewWidth = $previewWidth; - $this->maxPreviewHeight = $previewHeight; - } - } - - /** - * Limits a dimension to the maximum dimension provided as argument - * - * @param int $dim - * @param int $maxDim - * @param string $dimName - * - * @return integer - */ - private function limitMaxDim($dim, $maxDim, $dimName) { - if (!is_null($maxDim)) { - if ($dim > $maxDim) { - \OCP\Util::writeLog( - 'core', $dimName . ' reduced from ' . $dim . ' to ' . $maxDim, \OCP\Util::DEBUG - ); - $dim = $maxDim; - } - } - - return $dim; - } - - /** - * @param array $args - */ - public static function post_write($args) { - self::post_delete($args, 'files/'); - } - - /** - * @param array $args - */ - public static function prepare_delete_files($args) { - self::prepare_delete($args, 'files/'); - } - - /** - * @param array $args - * @param string $prefix - */ - public static function prepare_delete(array $args, $prefix = '') { - $path = $args['path']; - if (substr($path, 0, 1) === '/') { - $path = substr($path, 1); - } - - $view = new \OC\Files\View('/' . \OC_User::getUser() . '/' . $prefix); - - $absPath = Files\Filesystem::normalizePath($view->getAbsolutePath($path)); - $fileInfo = $view->getFileInfo($path); - if ($fileInfo === false) { - return; - } - self::addPathToDeleteFileMapper($absPath, $fileInfo); - if ($view->is_dir($path)) { - $children = self::getAllChildren($view, $path); - self::$deleteChildrenMapper[$absPath] = $children; - } - } - - /** - * @param string $absolutePath - * @param \OCP\Files\FileInfo $info - */ - private static function addPathToDeleteFileMapper($absolutePath, $info) { - self::$deleteFileMapper[$absolutePath] = $info; - } - - /** - * @param \OC\Files\View $view - * @param string $path - * - * @return array - */ - private static function getAllChildren($view, $path) { - $children = $view->getDirectoryContent($path); - $childrensFiles = array(); - - $fakeRootLength = strlen($view->getRoot()); - - for ($i = 0; $i < count($children); $i++) { - $child = $children[$i]; - - $childsPath = substr($child->getPath(), $fakeRootLength); - - if ($view->is_dir($childsPath)) { - $children = array_merge( - $children, - $view->getDirectoryContent($childsPath) - ); - } else { - $childrensFiles[] = $child; - } - } - - return $childrensFiles; - } - - /** - * @param array $args - */ - public static function post_delete_files($args) { - self::post_delete($args, 'files/'); - } - - /** - * @param array $args - */ - public static function post_delete_versions($args) { - self::post_delete($args, 'files/'); - } - - /** - * @param array $args - * @param string $prefix - */ - public static function post_delete($args, $prefix = '') { - $path = Files\Filesystem::normalizePath($args['path']); - - $preview = new Preview(\OC_User::getUser(), $prefix, $path); - $preview->deleteAllPreviews(); - } - -} diff --git a/lib/private/PreviewManager.php b/lib/private/PreviewManager.php index 36b3730a720..8c5a7ad29f1 100644 --- a/lib/private/PreviewManager.php +++ b/lib/private/PreviewManager.php @@ -68,6 +68,9 @@ class PreviewManager implements IPreview { /** @var array */ protected $defaultProviders; + /** @var string */ + protected $userId; + /** * PreviewManager constructor. * @@ -75,15 +78,18 @@ class PreviewManager implements IPreview { * @param IRootFolder $rootFolder * @param IAppData $appData * @param EventDispatcherInterface $eventDispatcher + * @param string $userId */ public function __construct(IConfig $config, IRootFolder $rootFolder, IAppData $appData, - EventDispatcherInterface $eventDispatcher) { + EventDispatcherInterface $eventDispatcher, + $userId) { $this->config = $config; $this->rootFolder = $rootFolder; $this->appData = $appData; $this->eventDispatcher = $eventDispatcher; + $this->userId = $userId; } /** @@ -144,10 +150,22 @@ class PreviewManager implements IPreview { * @param int $maxY The maximum Y size of the thumbnail. It can be smaller depending on the shape of the image * @param boolean $scaleUp Scale smaller images up to the thumbnail size or not. Might look ugly * @return \OCP\IImage + * @deprecated 11 Use getPreview */ public function createPreview($file, $maxX = 100, $maxY = 75, $scaleUp = false) { - $preview = new \OC\Preview('', '/', $file, $maxX, $maxY, $scaleUp); - return $preview->getPreview(); + try { + $userRoot = $this->rootFolder->getUserFolder($this->userId)->getParent(); + $node = $userRoot->get($file); + if (!($file instanceof File)) { + throw new NotFoundException(); + } + + $preview = $this->getPreview($node, $maxX, $maxY); + } catch (\Exception $e) { + return new \OC_Image(); + } + + return new \OC_Image($preview->getContent()); } /** diff --git a/lib/private/Repair.php b/lib/private/Repair.php index c212ea90744..e8d466cd844 100644 --- a/lib/private/Repair.php +++ b/lib/private/Repair.php @@ -31,12 +31,12 @@ namespace OC; use OC\Repair\AssetCache; -use OC\Repair\AvatarPermissions; use OC\Repair\CleanTags; use OC\Repair\Collation; use OC\Repair\DropOldJobs; use OC\Repair\MoveUpdaterStepFile; use OC\Repair\NC11\CleanPreviews; +use OC\Repair\NC11\FixMountStorages; use OC\Repair\NC11\MoveAvatars; use OC\Repair\OldGroupMembershipShares; use OC\Repair\RemoveGetETagEntries; @@ -47,7 +47,6 @@ use OC\Repair\SqliteAutoincrement; use OC\Repair\DropOldTables; use OC\Repair\FillETags; use OC\Repair\InnoDB; -use OC\Repair\RepairLegacyStorages; use OC\Repair\RepairMimeTypes; use OC\Repair\SearchLuceneTables; use OC\Repair\UpdateOutdatedOcsIds; @@ -132,7 +131,6 @@ class Repair implements IOutput{ return [ new Collation(\OC::$server->getConfig(), \OC::$server->getLogger(), \OC::$server->getDatabaseConnection(), false), new RepairMimeTypes(\OC::$server->getConfig()), - new RepairLegacyStorages(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection()), new AssetCache(), new FillETags(\OC::$server->getDatabaseConnection()), new CleanTags(\OC::$server->getDatabaseConnection(), \OC::$server->getUserManager()), @@ -143,7 +141,6 @@ class Repair implements IOutput{ new RepairInvalidShares(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection()), new SharePropagation(\OC::$server->getConfig()), new RemoveOldShares(\OC::$server->getDatabaseConnection()), - new AvatarPermissions(\OC::$server->getDatabaseConnection()), new RemoveRootShares(\OC::$server->getDatabaseConnection(), \OC::$server->getUserManager(), \OC::$server->getLazyRootFolder()), new RepairUnmergedShares( \OC::$server->getConfig(), @@ -161,6 +158,7 @@ class Repair implements IOutput{ \OC::$server->getUserManager(), \OC::$server->getConfig() ), + new FixMountStorages(\OC::$server->getDatabaseConnection()), ]; } diff --git a/lib/private/Repair/AvatarPermissions.php b/lib/private/Repair/AvatarPermissions.php deleted file mode 100644 index 9d75062358c..00000000000 --- a/lib/private/Repair/AvatarPermissions.php +++ /dev/null @@ -1,116 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OC\Repair; - -use Doctrine\DBAL\Platforms\OraclePlatform; -use OCP\IDBConnection; -use OCP\Migration\IOutput; -use OCP\Migration\IRepairStep; - -/** - * Class AvatarPermissions - * - * @package OC\Repair - */ -class AvatarPermissions implements IRepairStep { - /** @var IDBConnection */ - private $connection; - - /** - * AvatarPermissions constructor. - * - * @param IDBConnection $connection - */ - public function __construct(IDBConnection $connection) { - $this->connection = $connection; - } - - /** - * @return string - */ - public function getName() { - return 'Fix permissions so avatars can be stored again'; - } - - /** - * @param IOutput $output - */ - public function run(IOutput $output) { - $output->startProgress(2); - $this->fixUserRootPermissions(); - $output->advance(); - $this->fixAvatarPermissions(); - $output->finishProgress(); - } - - /** - * Make sure all user roots have permissions 23 (all but share) - */ - protected function fixUserRootPermissions() { - $qb = $this->connection->getQueryBuilder(); - $qb2 = $this->connection->getQueryBuilder(); - - $qb->select('numeric_id') - ->from('storages') - ->where($qb->expr()->like('id', $qb2->createParameter('like'))); - - if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) { - // '' is null on oracle - $path = $qb2->expr()->isNull('path'); - } else { - $path = $qb2->expr()->eq('path', $qb2->createNamedParameter('')); - } - - $qb2->update('filecache') - ->set('permissions', $qb2->createNamedParameter(23)) - ->where($path) - ->andWhere($qb2->expr()->in('storage', $qb2->createFunction($qb->getSQL()))) - ->andWhere($qb2->expr()->neq('permissions', $qb2->createNamedParameter(23))) - ->setParameter('like', 'home::%'); - - - $qb2->execute(); - } - - /** - * Make sure all avatar files in the user roots have permission 27 - */ - protected function fixAvatarPermissions() { - $qb = $this->connection->getQueryBuilder(); - $qb2 = $this->connection->getQueryBuilder(); - - $qb->select('numeric_id') - ->from('storages') - ->where($qb->expr()->like('id', $qb2->createParameter('like'))); - - $qb2->update('filecache') - ->set('permissions', $qb2->createNamedParameter(27)) - ->where($qb2->expr()->like('path', $qb2->createNamedParameter('avatar.%'))) - ->andWhere($qb2->expr()->in('storage', $qb2->createFunction($qb->getSQL()))) - ->andWhere($qb2->expr()->neq('permissions', $qb2->createNamedParameter(27))) - ->setParameter('like', 'home::%'); - - $qb2->execute(); - } - -} - diff --git a/lib/private/Repair/Collation.php b/lib/private/Repair/Collation.php index 54de1a719bd..a3535fb33a2 100644 --- a/lib/private/Repair/Collation.php +++ b/lib/private/Repair/Collation.php @@ -75,6 +75,18 @@ class Collation implements IRepairStep { $tables = $this->getAllNonUTF8BinTables($this->connection); foreach ($tables as $table) { + $output->info("Change row format for $table ..."); + $query = $this->connection->prepare('ALTER TABLE `' . $table . '` ROW_FORMAT = DYNAMIC;'); + try { + $query->execute(); + } catch (DriverException $e) { + // Just log this + $this->logger->logException($e); + if (!$this->ignoreFailures) { + throw $e; + } + } + $output->info("Change collation for $table ..."); $query = $this->connection->prepare('ALTER TABLE `' . $table . '` CONVERT TO CHARACTER SET ' . $characterSet . ' COLLATE ' . $characterSet . '_bin;'); try { diff --git a/lib/private/Repair/MoveUpdaterStepFile.php b/lib/private/Repair/MoveUpdaterStepFile.php index fabaff40d24..feb8a291282 100644 --- a/lib/private/Repair/MoveUpdaterStepFile.php +++ b/lib/private/Repair/MoveUpdaterStepFile.php @@ -44,7 +44,7 @@ class MoveUpdaterStepFile implements IRepairStep { public function run(IOutput $output) { - $dataDir = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT); + $dataDir = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data'); $instanceId = $this->config->getSystemValue('instanceid', null); if(!is_string($instanceId) || empty($instanceId)) { diff --git a/lib/private/Repair/NC11/FixMountStorages.php b/lib/private/Repair/NC11/FixMountStorages.php new file mode 100644 index 00000000000..d57a356dff9 --- /dev/null +++ b/lib/private/Repair/NC11/FixMountStorages.php @@ -0,0 +1,78 @@ +<?php +/** + * @copyright 2016 Joas Schilling <coding@schilljs.com> + * + * @author Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +namespace OC\Repair\NC11; + +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; + +class FixMountStorages implements IRepairStep { + + /** @var IDBConnection */ + private $db; + + /** + * @param IDBConnection $db + */ + public function __construct(IDBConnection $db) { + $this->db = $db; + } + + /** + * @return string + */ + public function getName() { + return 'Fix potential broken mount points'; + } + + public function run(IOutput $output) { + $query = $this->db->getQueryBuilder(); + $query->select('m.id', 'f.storage') + ->from('mounts', 'm') + ->leftJoin('m', 'filecache', 'f', $query->expr()->eq('m.root_id', 'f.fileid')) + ->where($query->expr()->neq('m.storage_id', 'f.storage')); + + $update = $this->db->getQueryBuilder(); + $update->update('mounts') + ->set('storage_id', $update->createParameter('storage')) + ->where($query->expr()->eq('id', $update->createParameter('mount'))); + + $result = $query->execute(); + $entriesUpdated = 0; + while ($row = $result->fetch()) { + $update->setParameter('storage', $row['storage'], IQueryBuilder::PARAM_INT) + ->setParameter('mount', $row['id'], IQueryBuilder::PARAM_INT); + $update->execute(); + $entriesUpdated++; + } + $result->closeCursor(); + + if ($entriesUpdated > 0) { + $output->info($entriesUpdated . ' mounts updated'); + return; + } + + $output->info('No mounts updated'); + } +} diff --git a/lib/private/Repair/NC11/MoveAvatarBackgroundJob.php b/lib/private/Repair/NC11/MoveAvatarsBackgroundJob.php index f8c0d9b3abf..f8c0d9b3abf 100644 --- a/lib/private/Repair/NC11/MoveAvatarBackgroundJob.php +++ b/lib/private/Repair/NC11/MoveAvatarsBackgroundJob.php diff --git a/lib/private/Repair/RepairInvalidShares.php b/lib/private/Repair/RepairInvalidShares.php index 6cb690057bb..04624c910dd 100644 --- a/lib/private/Repair/RepairInvalidShares.php +++ b/lib/private/Repair/RepairInvalidShares.php @@ -27,6 +27,7 @@ namespace OC\Repair; use OCP\Migration\IOutput; use OCP\Migration\IRepairStep; +use Doctrine\DBAL\Platforms\OraclePlatform; /** * Repairs shares with invalid data @@ -92,6 +93,26 @@ class RepairInvalidShares implements IRepairStep { } /** + * Adjust file share permissions + */ + private function adjustFileSharePermissions(IOutput $out) { + $mask = \OCP\Constants::PERMISSION_READ | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_SHARE; + $builder = $this->connection->getQueryBuilder(); + + $permsFunc = $builder->expr()->bitwiseAnd('permissions', $mask); + $builder + ->update('share') + ->set('permissions', $permsFunc) + ->where($builder->expr()->eq('item_type', $builder->expr()->literal('file'))) + ->andWhere($builder->expr()->neq('permissions', $permsFunc)); + + $updatedEntries = $builder->execute(); + if ($updatedEntries > 0) { + $out->info('Fixed file share permissions for ' . $updatedEntries . ' shares'); + } + } + + /** * Remove shares where the parent share does not exist anymore */ private function removeSharesNonExistingParent(IOutput $out) { @@ -137,6 +158,9 @@ class RepairInvalidShares implements IRepairStep { // this situation was only possible before 9.1 $this->addShareLinkDeletePermission($out); } + if (version_compare($ocVersionFromBeforeUpdate, '12.0.0.11', '<')) { + $this->adjustFileSharePermissions($out); + } $this->removeSharesNonExistingParent($out); } diff --git a/lib/private/Repair/RepairLegacyStorages.php b/lib/private/Repair/RepairLegacyStorages.php deleted file mode 100644 index 228bdb67fe8..00000000000 --- a/lib/private/Repair/RepairLegacyStorages.php +++ /dev/null @@ -1,257 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Aaron Wood <aaronjwood@gmail.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Repair; - -use OC\Files\Cache\Storage; -use OC\RepairException; -use OCP\Migration\IOutput; -use OCP\Migration\IRepairStep; - -class RepairLegacyStorages implements IRepairStep{ - /** - * @var \OCP\IConfig - */ - protected $config; - - /** - * @var \OCP\IDBConnection - */ - protected $connection; - - protected $findStorageInCacheStatement; - protected $renameStorageStatement; - - /** - * @param \OCP\IConfig $config - * @param \OCP\IDBConnection $connection - */ - public function __construct($config, $connection) { - $this->connection = $connection; - $this->config = $config; - - $this->findStorageInCacheStatement = $this->connection->prepare( - 'SELECT DISTINCT `storage` FROM `*PREFIX*filecache`' - . ' WHERE `storage` in (?, ?)' - ); - $this->renameStorageStatement = $this->connection->prepare( - 'UPDATE `*PREFIX*storages`' - . ' SET `id` = ?' - . ' WHERE `id` = ?' - ); - } - - public function getName() { - return 'Repair legacy storages'; - } - - /** - * Extracts the user id from a legacy storage id - * - * @param string $storageId legacy storage id in the - * format "local::/path/to/datadir/userid" - * @return string user id extracted from the storage id - */ - private function extractUserId($storageId) { - $storageId = rtrim($storageId, '/'); - $pos = strrpos($storageId, '/'); - return substr($storageId, $pos + 1); - } - - /** - * Fix the given legacy storage by renaming the old id - * to the new id. If the new id already exists, whichever - * storage that has data in the file cache will be used. - * If both have data, nothing will be done and false is - * returned. - * - * @param string $oldId old storage id - * @param int $oldNumericId old storage numeric id - * @param string $userId - * @return bool true if fixed, false otherwise - * @throws RepairException - */ - private function fixLegacyStorage($oldId, $oldNumericId, $userId = null) { - // check whether the new storage already exists - if (is_null($userId)) { - $userId = $this->extractUserId($oldId); - } - $newId = 'home::' . $userId; - - // check if target id already exists - $newNumericId = Storage::getNumericStorageId($newId); - if (!is_null($newNumericId)) { - $newNumericId = (int)$newNumericId; - // try and resolve the conflict - // check which one of "local::" or "home::" needs to be kept - $this->findStorageInCacheStatement->execute(array($oldNumericId, $newNumericId)); - $row1 = $this->findStorageInCacheStatement->fetch(); - $row2 = $this->findStorageInCacheStatement->fetch(); - $this->findStorageInCacheStatement->closeCursor(); - if ($row2 !== false) { - // two results means both storages have data, not auto-fixable - throw new RepairException( - 'Could not automatically fix legacy storage ' - . '"' . $oldId . '" => "' . $newId . '"' - . ' because they both have data.' - ); - } - if ($row1 === false || (int)$row1['storage'] === $oldNumericId) { - // old storage has data, then delete the empty new id - $toDelete = $newId; - } else if ((int)$row1['storage'] === $newNumericId) { - // new storage has data, then delete the empty old id - $toDelete = $oldId; - } else { - // unknown case, do not continue - return false; - } - - // delete storage including file cache - Storage::remove($toDelete); - - // if we deleted the old id, the new id will be used - // automatically - if ($toDelete === $oldId) { - // nothing more to do - return true; - } - } - - // rename old id to new id - $newId = Storage::adjustStorageId($newId); - $oldId = Storage::adjustStorageId($oldId); - $rowCount = $this->renameStorageStatement->execute(array($newId, $oldId)); - $this->renameStorageStatement->closeCursor(); - return ($rowCount === 1); - } - - /** - * Converts legacy home storage ids in the format - * "local::/data/dir/path/userid/" to the new format "home::userid" - */ - public function run(IOutput $out) { - // only run once - if ($this->config->getAppValue('core', 'repairlegacystoragesdone') === 'yes') { - return; - } - - $dataDir = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data/'); - $dataDir = rtrim($dataDir, '/') . '/'; - $dataDirId = 'local::' . $dataDir; - - $count = 0; - $hasWarnings = false; - - $this->connection->beginTransaction(); - - // note: not doing a direct UPDATE with the REPLACE function - // because regexp search/extract is needed and it is not guaranteed - // to work on all database types - $sql = 'SELECT `id`, `numeric_id` FROM `*PREFIX*storages`' - . ' WHERE `id` LIKE ?' - . ' ORDER BY `id`'; - $result = $this->connection->executeQuery($sql, array($this->connection->escapeLikeParameter($dataDirId) . '%')); - - while ($row = $result->fetch()) { - $currentId = $row['id']; - // one entry is the datadir itself - if ($currentId === $dataDirId) { - continue; - } - - try { - if ($this->fixLegacyStorage($currentId, (int)$row['numeric_id'])) { - $count++; - } - } - catch (RepairException $e) { - $hasWarnings = true; - $out->warning('Could not repair legacy storage ' . $currentId . ' automatically.'); - } - } - - // check for md5 ids, not in the format "prefix::" - $sql = 'SELECT COUNT(*) AS "c" FROM `*PREFIX*storages`' - . ' WHERE `id` NOT LIKE \'%::%\''; - $result = $this->connection->executeQuery($sql); - $row = $result->fetch(); - - // find at least one to make sure it's worth - // querying the user list - if ((int)$row['c'] > 0) { - $userManager = \OC::$server->getUserManager(); - - // use chunks to avoid caching too many users in memory - $limit = 30; - $offset = 0; - - do { - // query the next page of users - $results = $userManager->search('', $limit, $offset); - $storageIds = array(); - foreach ($results as $uid => $userObject) { - $storageId = $dataDirId . $uid . '/'; - if (strlen($storageId) <= 64) { - // skip short storage ids as they were handled in the previous section - continue; - } - $storageIds[$uid] = $storageId; - } - - if (count($storageIds) > 0) { - // update the storages of these users - foreach ($storageIds as $uid => $storageId) { - $numericId = Storage::getNumericStorageId($storageId); - try { - if (!is_null($numericId) && $this->fixLegacyStorage($storageId, (int)$numericId)) { - $count++; - } - } - catch (RepairException $e) { - $hasWarnings = true; - $out->warning('Could not repair legacy storage ' . $storageId . ' automatically.'); - } - } - } - $offset += $limit; - } while (count($results) >= $limit); - } - - $out->info('Updated ' . $count . ' legacy home storage ids'); - - $this->connection->commit(); - - Storage::getGlobalCache()->clearCache(); - - if ($hasWarnings) { - $out->warning('Some legacy storages could not be repaired. Please manually fix them then re-run ./occ maintenance:repair'); - } else { - // if all were done, no need to redo the repair during next upgrade - $this->config->setAppValue('core', 'repairlegacystoragesdone', 'yes'); - } - } -} diff --git a/lib/private/Security/Bruteforce/Throttler.php b/lib/private/Security/Bruteforce/Throttler.php index 031c5ffd411..765f109fdb3 100644 --- a/lib/private/Security/Bruteforce/Throttler.php +++ b/lib/private/Security/Bruteforce/Throttler.php @@ -189,9 +189,10 @@ class Throttler { * Get the throttling delay (in milliseconds) * * @param string $ip + * @param string $action optionally filter by action * @return int */ - public function getDelay($ip) { + public function getDelay($ip, $action = '') { $cutoffTime = (new \DateTime()) ->sub($this->getCutoff(43200)) ->getTimestamp(); @@ -201,6 +202,11 @@ class Throttler { ->from('bruteforce_attempts') ->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime))) ->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($this->getSubnet($ip)))); + + if ($action !== '') { + $qb->andWhere($qb->expr()->eq('action', $qb->createNamedParameter($action))); + } + $attempts = count($qb->execute()->fetchAll()); if ($attempts === 0) { @@ -225,10 +231,11 @@ class Throttler { * Will sleep for the defined amount of time * * @param string $ip + * @param string $action optionally filter by action * @return int the time spent sleeping */ - public function sleepDelay($ip) { - $delay = $this->getDelay($ip); + public function sleepDelay($ip, $action = '') { + $delay = $this->getDelay($ip, $action); usleep($delay * 1000); return $delay; } diff --git a/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php b/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php index 284700566d6..85ae127f5f1 100644 --- a/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php +++ b/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php @@ -69,8 +69,6 @@ class ContentSecurityPolicyNonceManager { Request::USER_AGENT_CHROME, // Firefox 45+ '/^Mozilla\/5\.0 \([^)]+\) Gecko\/[0-9.]+ Firefox\/(4[5-9]|[5-9][0-9])\.[0-9.]+$/', - // Safari 10+ - '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Version\/1[0-9.]+ Safari\/[0-9.A-Z]+$/', ]; if($this->request->isUserAgent($browserWhitelist)) { diff --git a/lib/private/Security/CertificateManager.php b/lib/private/Security/CertificateManager.php index f7bf0df58c5..461ef9457a7 100644 --- a/lib/private/Security/CertificateManager.php +++ b/lib/private/Security/CertificateManager.php @@ -30,6 +30,7 @@ namespace OC\Security; use OC\Files\Filesystem; use OCP\ICertificateManager; use OCP\IConfig; +use OCP\ILogger; /** * Manage trusted certificates for users @@ -51,14 +52,21 @@ class CertificateManager implements ICertificateManager { protected $config; /** + * @var ILogger + */ + protected $logger; + + /** * @param string $uid * @param \OC\Files\View $view relative to data/ * @param IConfig $config + * @param ILogger $logger */ - public function __construct($uid, \OC\Files\View $view, IConfig $config) { + public function __construct($uid, \OC\Files\View $view, IConfig $config, ILogger $logger) { $this->uid = $uid; $this->view = $view; $this->config = $config; + $this->logger = $logger; } /** @@ -104,6 +112,13 @@ class CertificateManager implements ICertificateManager { $this->view->mkdir($path); } + $defaultCertificates = file_get_contents(\OC::$SERVERROOT . '/resources/config/ca-bundle.crt'); + if (strlen($defaultCertificates) < 1024) { // sanity check to verify that we have some content for our bundle + // log as exception so we have a stacktrace + $this->logger->logException(new \Exception('Shipped ca-bundle is empty, refusing to create certificate bundle')); + return; + } + $fhCerts = $this->view->fopen($path . '/rootcerts.crt', 'w'); // Write user certificates @@ -117,7 +132,6 @@ class CertificateManager implements ICertificateManager { } // Append the default certificates - $defaultCertificates = file_get_contents(\OC::$SERVERROOT . '/resources/config/ca-bundle.crt'); fwrite($fhCerts, $defaultCertificates); // Append the system certificate bundle @@ -203,7 +217,7 @@ class CertificateManager implements ICertificateManager { } if ($this->needsRebundling($uid)) { if (is_null($uid)) { - $manager = new CertificateManager(null, $this->view, $this->config); + $manager = new CertificateManager(null, $this->view, $this->config, $this->logger); $manager->createCertificateBundle(); } else { $this->createCertificateBundle(); diff --git a/lib/private/Server.php b/lib/private/Server.php index 2c0aac9b43c..3c716ae6ce6 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -45,8 +45,8 @@ use bantu\IniGetWrapper\IniGetWrapper; use OC\App\AppStore\Fetcher\AppFetcher; use OC\App\AppStore\Fetcher\CategoryFetcher; use OC\AppFramework\Http\Request; -use OC\AppFramework\Db\Db; use OC\AppFramework\Utility\TimeFactory; +use OC\Authentication\LoginCredentials\Store; use OC\Command\AsyncBus; use OC\Diagnostics\EventLogger; use OC\Diagnostics\NullEventLogger; @@ -90,6 +90,7 @@ use OC\Security\TrustedDomainHelper; use OC\Session\CryptoWrapper; use OC\Tagging\TagMapper; use OCA\Theming\ThemingDefaults; +use OCP\Authentication\LoginCredentials\IStore; use OCP\IL10N; use OCP\IServerContainer; use OCP\RichObjectStrings\IValidator; @@ -125,7 +126,8 @@ class Server extends ServerContainer implements IServerContainer { $c->getConfig(), $c->getRootFolder(), $c->getAppDataDir('preview'), - $c->getEventDispatcher() + $c->getEventDispatcher(), + $c->getSession()->get('user_id') ); }); @@ -246,6 +248,17 @@ class Server extends ServerContainer implements IServerContainer { }); return $groupManager; }); + $this->registerService(Store::class, function(Server $c) { + $session = $c->getSession(); + if (\OC::$server->getSystemConfig()->getValue('installed', false)) { + $tokenProvider = $c->query('OC\Authentication\Token\IProvider'); + } else { + $tokenProvider = null; + } + $logger = $c->getLogger(); + return new Store($session, $logger, $tokenProvider); + }); + $this->registerAlias(IStore::class, Store::class); $this->registerService('OC\Authentication\Token\DefaultTokenMapper', function (Server $c) { $dbConnection = $c->getDatabaseConnection(); return new Authentication\Token\DefaultTokenMapper($dbConnection); @@ -313,11 +326,15 @@ class Server extends ServerContainer implements IServerContainer { }); $this->registerService(\OC\Authentication\TwoFactorAuth\Manager::class, function (Server $c) { - return new \OC\Authentication\TwoFactorAuth\Manager($c->getAppManager(), $c->getSession(), $c->getConfig()); + return new \OC\Authentication\TwoFactorAuth\Manager($c->getAppManager(), $c->getSession(), $c->getConfig(), $c->getActivityManager(), $c->getLogger()); }); - $this->registerService('NavigationManager', function ($c) { - return new \OC\NavigationManager(); + $this->registerService('NavigationManager', function (Server $c) { + return new \OC\NavigationManager($c->getAppManager(), + $c->getURLGenerator(), + $c->getL10NFactory(), + $c->getUserSession(), + $c->getGroupManager()); }); $this->registerService('AllConfig', function (Server $c) { return new \OC\AllConfig( @@ -361,7 +378,8 @@ class Server extends ServerContainer implements IServerContainer { return new CategoryFetcher( $this->getAppDataDir('appstore'), $this->getHTTPClientService(), - $this->query(TimeFactory::class) + $this->query(TimeFactory::class), + $this->getConfig() ); }); $this->registerService('UserCache', function ($c) { @@ -376,7 +394,7 @@ class Server extends ServerContainer implements IServerContainer { $version = implode(',', $v); $instanceId = \OC_Util::getInstanceId(); $path = \OC::$SERVERROOT; - $prefix = md5($instanceId . '-' . $version . '-' . $path); + $prefix = md5($instanceId . '-' . $version . '-' . $path . '-' . \OC::$WEBROOT); return new \OC\Memcache\Factory($prefix, $c->getLogger(), $config->getSystemValue('memcache.local', null), $config->getSystemValue('memcache.distributed', null), @@ -418,9 +436,8 @@ class Server extends ServerContainer implements IServerContainer { ); }); $this->registerService('Logger', function (Server $c) { - $logClass = $c->query('AllConfig')->getSystemValue('log_type', 'file'); - // TODO: Drop backwards compatibility for config in the future - $logger = 'OC\\Log\\' . ucfirst($logClass=='owncloud' ? 'file' : $logClass); + $logType = $c->query('AllConfig')->getSystemValue('log_type', 'file'); + $logger = Log::getLogClass($logType); call_user_func(array($logger, 'init')); return new Log($logger); @@ -470,9 +487,6 @@ class Server extends ServerContainer implements IServerContainer { $connection->getConfiguration()->setSQLLogger($c->getQueryLogger()); return $connection; }); - $this->registerService('Db', function (Server $c) { - return new Db($c->getDatabaseConnection()); - }); $this->registerService('HTTPHelper', function (Server $c) { $config = $c->getConfig(); return new HTTPHelper( @@ -485,7 +499,7 @@ class Server extends ServerContainer implements IServerContainer { $uid = $user ? $user : null; return new ClientService( $c->getConfig(), - new \OC\Security\CertificateManager($uid, new View(), $c->getConfig()) + new \OC\Security\CertificateManager($uid, new View(), $c->getConfig(), $c->getLogger()) ); }); $this->registerService('EventLogger', function (Server $c) { @@ -794,7 +808,9 @@ class Server extends ServerContainer implements IServerContainer { $c->getConfig(), $c->getEncryptionManager(), $c->getUserManager(), - $c->getLockingProvider() + $c->getLockingProvider(), + new \OC\Settings\Mapper($c->getDatabaseConnection()), + $c->getURLGenerator() ); return $manager; }); @@ -900,7 +916,6 @@ class Server extends ServerContainer implements IServerContainer { return $this->query('SystemTagObjectMapper'); } - /** * Returns the avatar manager, used for avatar functionality * @@ -998,7 +1013,8 @@ class Server extends ServerContainer implements IServerContainer { */ public function setSession(\OCP\ISession $session) { $this->query(SessionStorage::class)->setSession($session); - return $this->query('UserSession')->setSession($session); + $this->query('UserSession')->setSession($session); + $this->query(Store::class)->setSession($session); } /** @@ -1199,16 +1215,6 @@ class Server extends ServerContainer implements IServerContainer { } /** - * Returns an instance of the db facade - * - * @deprecated use getDatabaseConnection, will be removed in ownCloud 10 - * @return \OCP\IDb - */ - public function getDb() { - return $this->query('Db'); - } - - /** * Returns an instance of the HTTP helper class * * @deprecated Use getHTTPClientService() @@ -1233,7 +1239,7 @@ class Server extends ServerContainer implements IServerContainer { } $userId = $user->getUID(); } - return new CertificateManager($userId, new View(), $this->getConfig()); + return new CertificateManager($userId, new View(), $this->getConfig(), $this->getLogger()); } /** diff --git a/lib/private/Settings/Admin/Encryption.php b/lib/private/Settings/Admin/Encryption.php index 6e93407f1a3..63020c6bce7 100644 --- a/lib/private/Settings/Admin/Encryption.php +++ b/lib/private/Settings/Admin/Encryption.php @@ -23,23 +23,23 @@ namespace OC\Settings\Admin; -use OC\Encryption\Manager; use OCP\AppFramework\Http\TemplateResponse; +use OCP\Encryption\IManager; use OCP\IUserManager; use OCP\Settings\ISettings; class Encryption implements ISettings { - /** @var Manager */ + /** @var IManager */ private $manager; /** @var IUserManager */ private $userManager; /** - * @param Manager $manager + * @param IManager $manager * @param IUserManager $userManager */ - public function __construct(Manager $manager, IUserManager $userManager) { + public function __construct(IManager $manager, IUserManager $userManager) { $this->manager = $manager; $this->userManager = $userManager; } diff --git a/lib/private/Settings/Manager.php b/lib/private/Settings/Manager.php index 990750848d3..7a339b94199 100644 --- a/lib/private/Settings/Manager.php +++ b/lib/private/Settings/Manager.php @@ -29,6 +29,7 @@ use OCP\IConfig; use OCP\IDBConnection; use OCP\IL10N; use OCP\ILogger; +use OCP\IURLGenerator; use OCP\IUserManager; use OCP\Lock\ILockingProvider; use OCP\Settings\ISettings; @@ -43,6 +44,8 @@ class Manager implements IManager { private $log; /** @var IDBConnection */ private $dbc; + /** @var Mapper */ + private $mapper; /** @var IL10N */ private $l; /** @var IConfig */ @@ -53,6 +56,8 @@ class Manager implements IManager { private $userManager; /** @var ILockingProvider */ private $lockingProvider; + /** @var IURLGenerator */ + private $url; /** * @param ILogger $log @@ -62,6 +67,8 @@ class Manager implements IManager { * @param EncryptionManager $encryptionManager * @param IUserManager $userManager * @param ILockingProvider $lockingProvider + * @param Mapper $mapper + * @param IURLGenerator $url */ public function __construct( ILogger $log, @@ -70,25 +77,29 @@ class Manager implements IManager { IConfig $config, EncryptionManager $encryptionManager, IUserManager $userManager, - ILockingProvider $lockingProvider + ILockingProvider $lockingProvider, + Mapper $mapper, + IURLGenerator $url ) { $this->log = $log; $this->dbc = $dbc; + $this->mapper = $mapper; $this->l = $l; $this->config = $config; $this->encryptionManager = $encryptionManager; $this->userManager = $userManager; $this->lockingProvider = $lockingProvider; + $this->url = $url; } /** * @inheritdoc */ public function setupSettings(array $settings) { - if(isset($settings[IManager::KEY_ADMIN_SECTION])) { + if (isset($settings[IManager::KEY_ADMIN_SECTION])) { $this->setupAdminSection($settings[IManager::KEY_ADMIN_SECTION]); } - if(isset($settings[IManager::KEY_ADMIN_SETTINGS])) { + if (isset($settings[IManager::KEY_ADMIN_SETTINGS])) { $this->setupAdminSettings($settings[IManager::KEY_ADMIN_SETTINGS]); } } @@ -104,50 +115,33 @@ class Manager implements IManager { public function onAppDisabled($appId) { $appInfo = \OC_App::getAppInfo($appId); // hello static legacy - if(isset($appInfo['settings'][IManager::KEY_ADMIN_SECTION])) { - $this->remove(self::TABLE_ADMIN_SECTIONS, trim($appInfo['settings'][IManager::KEY_ADMIN_SECTION], '\\')); + if (isset($appInfo['settings'][IManager::KEY_ADMIN_SECTION])) { + $this->mapper->remove(self::TABLE_ADMIN_SECTIONS, trim($appInfo['settings'][IManager::KEY_ADMIN_SECTION], '\\')); } - if(isset($appInfo['settings'][IManager::KEY_ADMIN_SETTINGS])) { - $this->remove(self::TABLE_ADMIN_SETTINGS, trim($appInfo['settings'][IManager::KEY_ADMIN_SETTINGS], '\\')); + if (isset($appInfo['settings'][IManager::KEY_ADMIN_SETTINGS])) { + $this->mapper->remove(self::TABLE_ADMIN_SETTINGS, trim($appInfo['settings'][IManager::KEY_ADMIN_SETTINGS], '\\')); } } public function checkForOrphanedClassNames() { - $tables = [ self::TABLE_ADMIN_SECTIONS, self::TABLE_ADMIN_SETTINGS ]; + $tables = [self::TABLE_ADMIN_SECTIONS, self::TABLE_ADMIN_SETTINGS]; foreach ($tables as $table) { - $classes = $this->getClasses($table); - foreach($classes as $className) { + $classes = $this->mapper->getClasses($table); + foreach ($classes as $className) { try { \OC::$server->query($className); } catch (QueryException $e) { - $this->remove($table, $className); + $this->mapper->remove($table, $className); } } } } /** - * returns the registerd classes in the given table - * - * @param $table - * @return string[] - */ - private function getClasses($table) { - $q = $this->dbc->getQueryBuilder(); - $resultStatement = $q->select('class') - ->from($table) - ->execute(); - $data = $resultStatement->fetchAll(); - $resultStatement->closeCursor(); - - return array_map(function($row) { return $row['class']; }, $data); - } - - /** * @param string $sectionClassName */ private function setupAdminSection($sectionClassName) { - if(!class_exists($sectionClassName)) { + if (!class_exists($sectionClassName)) { $this->log->debug('Could not find admin section class ' . $sectionClassName); return; } @@ -158,14 +152,14 @@ class Manager implements IManager { return; } - if(!$section instanceof ISection) { + if (!$section instanceof ISection) { $this->log->error( 'Admin section instance must implement \OCP\ISection. Invalid class: {class}', ['class' => $sectionClassName] ); return; } - if(!$this->hasAdminSection(get_class($section))) { + if (!$this->hasAdminSection(get_class($section))) { $this->addAdminSection($section); } else { $this->updateAdminSection($section); @@ -173,7 +167,7 @@ class Manager implements IManager { } private function addAdminSection(ISection $section) { - $this->add(self::TABLE_ADMIN_SECTIONS, [ + $this->mapper->add(self::TABLE_ADMIN_SECTIONS, [ 'id' => $section->getID(), 'class' => get_class($section), 'priority' => $section->getPriority(), @@ -181,28 +175,15 @@ class Manager implements IManager { } private function addAdminSettings(ISettings $settings) { - $this->add(self::TABLE_ADMIN_SETTINGS, [ + $this->mapper->add(self::TABLE_ADMIN_SETTINGS, [ 'class' => get_class($settings), 'section' => $settings->getSection(), 'priority' => $settings->getPriority(), ]); } - /** - * @param string $table - * @param array $values - */ - private function add($table, array $values) { - $query = $this->dbc->getQueryBuilder(); - $values = array_map(function($value) use ($query) { - return $query->createNamedParameter($value); - }, $values); - $query->insert($table)->values($values); - $query->execute(); - } - private function updateAdminSettings(ISettings $settings) { - $this->update( + $this->mapper->update( self::TABLE_ADMIN_SETTINGS, 'class', get_class($settings), @@ -214,35 +195,23 @@ class Manager implements IManager { } private function updateAdminSection(ISection $section) { - $this->update( + $this->mapper->update( self::TABLE_ADMIN_SECTIONS, 'class', get_class($section), [ - 'id' => $section->getID(), + 'id' => $section->getID(), 'priority' => $section->getPriority(), ] ); } - private function update($table, $idCol, $id, $values) { - $query = $this->dbc->getQueryBuilder(); - $query->update($table); - foreach($values as $key => $value) { - $query->set($key, $query->createNamedParameter($value)); - } - $query - ->where($query->expr()->eq($idCol, $query->createParameter($idCol))) - ->setParameter($idCol, $id) - ->execute(); - } - /** * @param string $className * @return bool */ private function hasAdminSection($className) { - return $this->has(self::TABLE_ADMIN_SECTIONS, $className); + return $this->mapper->has(self::TABLE_ADMIN_SECTIONS, $className); } /** @@ -250,44 +219,11 @@ class Manager implements IManager { * @return bool */ private function hasAdminSettings($className) { - return $this->has(self::TABLE_ADMIN_SETTINGS, $className); - } - - /** - * @param string $table - * @param string $className - * @return bool - */ - private function has($table, $className) { - $query = $this->dbc->getQueryBuilder(); - $query->select('class') - ->from($table) - ->where($query->expr()->eq('class', $query->createNamedParameter($className))) - ->setMaxResults(1); - - $result = $query->execute(); - $row = $result->fetch(); - $result->closeCursor(); - - return (bool) $row; - } - - /** - * deletes an settings or admin entry from the given table - * - * @param $table - * @param $className - */ - private function remove($table, $className) { - $query = $this->dbc->getQueryBuilder(); - $query->delete($table) - ->where($query->expr()->eq('class', $query->createNamedParameter($className))); - - $query->execute(); + return $this->mapper->has(self::TABLE_ADMIN_SETTINGS, $className); } private function setupAdminSettings($settingsClassName) { - if(!class_exists($settingsClassName)) { + if (!class_exists($settingsClassName)) { $this->log->debug('Could not find admin section class ' . $settingsClassName); return; } @@ -300,14 +236,14 @@ class Manager implements IManager { return; } - if(!$settings instanceof ISettings) { + if (!$settings instanceof ISettings) { $this->log->error( 'Admin section instance must implement \OCP\Settings\ISection. Invalid class: {class}', ['class' => $settingsClassName] ); return; } - if(!$this->hasAdminSettings(get_class($settings))) { + if (!$this->hasAdminSettings(get_class($settings))) { $this->addAdminSettings($settings); } else { $this->updateAdminSettings($settings); @@ -329,24 +265,17 @@ class Manager implements IManager { public function getAdminSections() { // built-in sections $sections = [ - 0 => [new Section('server', $this->l->t('Server settings'), 0)], - 5 => [new Section('sharing', $this->l->t('Sharing'), 0)], - 45 => [new Section('encryption', $this->l->t('Encryption'), 0)], - 98 => [new Section('additional', $this->l->t('Additional settings'), 0)], - 99 => [new Section('tips-tricks', $this->l->t('Tips & tricks'), 0)], + 0 => [new Section('server', $this->l->t('Server settings'), 0, $this->url->imagePath('settings', 'admin.svg'))], + 5 => [new Section('sharing', $this->l->t('Sharing'), 0, $this->url->imagePath('core', 'actions/share.svg'))], + 45 => [new Section('encryption', $this->l->t('Encryption'), 0, $this->url->imagePath('core', 'actions/password.svg'))], + 98 => [new Section('additional', $this->l->t('Additional settings'), 0, $this->url->imagePath('core', 'actions/settings-dark.svg'))], + 99 => [new Section('tips-tricks', $this->l->t('Tips & tricks'), 0, $this->url->imagePath('settings', 'help.svg'))], ]; - $query = $this->dbc->getQueryBuilder(); - $query->selectDistinct('s.class') - ->addSelect('s.priority') - ->from(self::TABLE_ADMIN_SECTIONS, 's') - ->from(self::TABLE_ADMIN_SETTINGS, 'f') - ->where($query->expr()->eq('s.id', 'f.section')) - ; - $result = $query->execute(); + $rows = $this->mapper->getAdminSectionsFromDB(); - while($row = $result->fetch()) { - if(!isset($sections[$row['priority']])) { + foreach ($rows as $row) { + if (!isset($sections[$row['priority']])) { $sections[$row['priority']] = []; } try { @@ -355,38 +284,42 @@ class Manager implements IManager { // skip } } - $result->closeCursor(); ksort($sections); + return $sections; } + /** + * @param string $section + * @return ISection[] + */ private function getBuiltInAdminSettings($section) { $forms = []; try { - if($section === 'server') { + if ($section === 'server') { /** @var ISettings $form */ $form = new Admin\Server($this->dbc, $this->config, $this->lockingProvider, $this->l); $forms[$form->getPriority()] = [$form]; $form = new Admin\ServerDevNotice(); $forms[$form->getPriority()] = [$form]; } - if($section === 'encryption') { + if ($section === 'encryption') { /** @var ISettings $form */ $form = new Admin\Encryption($this->encryptionManager, $this->userManager); $forms[$form->getPriority()] = [$form]; } - if($section === 'sharing') { + if ($section === 'sharing') { /** @var ISettings $form */ $form = new Admin\Sharing($this->config); $forms[$form->getPriority()] = [$form]; } - if($section === 'additional') { + if ($section === 'additional') { /** @var ISettings $form */ $form = new Admin\Additional($this->config); $forms[$form->getPriority()] = [$form]; } - if($section === 'tips-tricks') { + if ($section === 'tips-tricks') { /** @var ISettings $form */ $form = new Admin\TipsTricks($this->config); $forms[$form->getPriority()] = [$form]; @@ -397,16 +330,15 @@ class Manager implements IManager { return $forms; } - private function getAdminSettingsFromDB($section, &$settings) { - $query = $this->dbc->getQueryBuilder(); - $query->select(['class', 'priority']) - ->from(self::TABLE_ADMIN_SETTINGS) - ->where($query->expr()->eq('section', $this->dbc->getQueryBuilder()->createParameter('section'))) - ->setParameter('section', $section); + /** + * @inheritdoc + */ + public function getAdminSettings($section) { + $settings = $this->getBuiltInAdminSettings($section); + $dbRows = $this->mapper->getAdminSettingsFromDB($section); - $result = $query->execute(); - while($row = $result->fetch()) { - if(!isset($settings[$row['priority']])) { + foreach ($dbRows as $row) { + if (!isset($settings[$row['priority']])) { $settings[$row['priority']] = []; } try { @@ -415,17 +347,8 @@ class Manager implements IManager { // skip } } - $result->closeCursor(); ksort($settings); - } - - /** - * @inheritdoc - */ - public function getAdminSettings($section) { - $settings = $this->getBuiltInAdminSettings($section); - $this->getAdminSettingsFromDB($section, $settings); return $settings; } } diff --git a/lib/private/Settings/Mapper.php b/lib/private/Settings/Mapper.php new file mode 100644 index 00000000000..2525f2c9854 --- /dev/null +++ b/lib/private/Settings/Mapper.php @@ -0,0 +1,163 @@ +<?php +/** + * @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl> + * + * @author Robin Appelman <robin@icewind.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Settings; + +use OCP\IDBConnection; + +class Mapper { + const TABLE_ADMIN_SETTINGS = 'admin_settings'; + const TABLE_ADMIN_SECTIONS = 'admin_sections'; + + /** @var IDBConnection */ + private $dbc; + + /** + * @param IDBConnection $dbc + */ + public function __construct(IDBConnection $dbc) { + $this->dbc = $dbc; + } + + /** + * Get the configured admin settings from the database for the provided section + * + * @param string $section + * @return array[] [['class' => string, 'priority' => int], ...] + */ + public function getAdminSettingsFromDB($section) { + $query = $this->dbc->getQueryBuilder(); + $query->select(['class', 'priority']) + ->from(self::TABLE_ADMIN_SETTINGS) + ->where($query->expr()->eq('section', $this->dbc->getQueryBuilder()->createParameter('section'))) + ->setParameter('section', $section); + + $result = $query->execute(); + return $result->fetchAll(); + } + + /** + * Get the configured admin sections from the database + * + * @return array[] [['class' => string, 'priority' => int], ...] + */ + public function getAdminSectionsFromDB() { + $query = $this->dbc->getQueryBuilder(); + $query->selectDistinct('s.class') + ->addSelect('s.priority') + ->from(self::TABLE_ADMIN_SECTIONS, 's') + ->from(self::TABLE_ADMIN_SETTINGS, 'f') + ->where($query->expr()->eq('s.id', 'f.section')); + $result = $query->execute(); + return array_map(function ($row) { + $row['priority'] = (int)$row['priority']; + return $row; + }, $result->fetchAll()); + } + + /** + * @param string $table Mapper::TABLE_ADMIN_SECTIONS or Mapper::TABLE_ADMIN_SETTINGS + * @param array $values + */ + public function add($table, array $values) { + $query = $this->dbc->getQueryBuilder(); + $values = array_map(function ($value) use ($query) { + return $query->createNamedParameter($value); + }, $values); + $query->insert($table)->values($values); + $query->execute(); + } + + /** + * returns the registered classes in the given table + * + * @param $table Mapper::TABLE_ADMIN_SECTIONS or Mapper::TABLE_ADMIN_SETTINGS + * @return string[] + */ + public function getClasses($table) { + $q = $this->dbc->getQueryBuilder(); + $resultStatement = $q->select('class') + ->from($table) + ->execute(); + $data = $resultStatement->fetchAll(); + $resultStatement->closeCursor(); + + return array_map(function ($row) { + return $row['class']; + }, $data); + } + + /** + * Check if a class is configured in the database + * + * @param string $table Mapper::TABLE_ADMIN_SECTIONS or Mapper::TABLE_ADMIN_SETTINGS + * @param string $className + * @return bool + */ + public function has($table, $className) { + $query = $this->dbc->getQueryBuilder(); + $query->select('class') + ->from($table) + ->where($query->expr()->eq('class', $query->createNamedParameter($className))) + ->setMaxResults(1); + + $result = $query->execute(); + $row = $result->fetch(); + $result->closeCursor(); + + return (bool)$row; + } + + /** + * deletes an settings or admin entry from the given table + * + * @param $table Mapper::TABLE_ADMIN_SECTIONS or Mapper::TABLE_ADMIN_SETTINGS + * @param $className + */ + public function remove($table, $className) { + $query = $this->dbc->getQueryBuilder(); + $query->delete($table) + ->where($query->expr()->eq('class', $query->createNamedParameter($className))); + + $query->execute(); + } + + /** + * @param $table Mapper::TABLE_ADMIN_SECTIONS or Mapper::TABLE_ADMIN_SETTINGS + * @param $idCol + * @param $id + * @param $values + */ + public function update($table, $idCol, $id, $values) { + $query = $this->dbc->getQueryBuilder(); + $query->update($table); + foreach ($values as $key => $value) { + $query->set($key, $query->createNamedParameter($value)); + } + $query + ->where($query->expr()->eq($idCol, $query->createParameter($idCol))) + ->setParameter($idCol, $id) + ->execute(); + } + +} diff --git a/lib/private/Settings/Section.php b/lib/private/Settings/Section.php index b3cf242279f..c89a3999c4e 100644 --- a/lib/private/Settings/Section.php +++ b/lib/private/Settings/Section.php @@ -23,25 +23,29 @@ namespace OC\Settings; -use OCP\Settings\ISection; +use OCP\Settings\IIconSection; -class Section implements ISection { +class Section implements IIconSection { /** @var string */ private $id; /** @var string */ private $name; /** @var int */ private $priority; + /** @var string */ + private $icon; /** * @param string $id * @param string $name * @param int $priority + * @param string $icon */ - public function __construct($id, $name, $priority) { + public function __construct($id, $name, $priority, $icon = '') { $this->id = $id; $this->name = $name; $this->priority = $priority; + $this->icon = $icon; } /** @@ -74,4 +78,15 @@ class Section implements ISection { public function getPriority() { return $this->priority; } + + /** + * returns the relative path to an 16*16 icon describing the section. + * e.g. '/core/img/places/files.svg' + * + * @returns string + * @since 12 + */ + public function getIcon() { + return $this->icon; + } } diff --git a/lib/private/Setup.php b/lib/private/Setup.php index 81a5343fe21..d9997767684 100644 --- a/lib/private/Setup.php +++ b/lib/private/Setup.php @@ -125,8 +125,8 @@ class Setup { public function getSupportedDatabases($allowAllDatabases = false) { $availableDatabases = array( 'sqlite' => array( - 'type' => 'class', - 'call' => 'SQLite3', + 'type' => 'pdo', + 'call' => 'sqlite', 'name' => 'SQLite' ), 'mysql' => array( @@ -163,9 +163,7 @@ class Setup { $type = $availableDatabases[$database]['type']; $call = $availableDatabases[$database]['call']; - if($type === 'class') { - $working = $this->class_exists($call); - } elseif ($type === 'function') { + if ($type === 'function') { $working = $this->is_callable($call); } elseif($type === 'pdo') { $working = in_array($call, $this->getAvailableDbDriversForPdo(), TRUE); diff --git a/lib/private/Setup/MySQL.php b/lib/private/Setup/MySQL.php index d1399c8821c..bafb3866b76 100644 --- a/lib/private/Setup/MySQL.php +++ b/lib/private/Setup/MySQL.php @@ -35,7 +35,7 @@ class MySQL extends AbstractDatabase { public function setupDatabase($username) { //check if the database user has admin right - $connection = $this->connect(); + $connection = $this->connect(['dbname' => null]); $this->createSpecificUser($username, $connection); @@ -152,7 +152,7 @@ class MySQL extends AbstractDatabase { }; } } catch (\Exception $ex) { - $this->logger->error('Specific user creation failed: {error}', [ + $this->logger->info('Can not create a new MySQL user, will continue with the provided user: {error}', [ 'app' => 'mysql.setup', 'error' => $ex->getMessage() ]); diff --git a/lib/private/Setup/PostgreSQL.php b/lib/private/Setup/PostgreSQL.php index 085e8609ab2..c01e5bc0332 100644 --- a/lib/private/Setup/PostgreSQL.php +++ b/lib/private/Setup/PostgreSQL.php @@ -150,14 +150,16 @@ class PostgreSQL extends AbstractDatabase { } private function createDBUser(IDBConnection $connection) { + $dbUser = $this->dbUser; try { - if ($this->userExists($connection)) { - // change the password - $query = $connection->prepare("ALTER ROLE " . addslashes($this->dbUser) . " WITH CREATEDB PASSWORD '" . addslashes($this->dbPassword) . "'"); - } else { - // create the user - $query = $connection->prepare("CREATE USER " . addslashes($this->dbUser) . " CREATEDB PASSWORD '" . addslashes($this->dbPassword) . "'"); - } + $i = 1; + while ($this->userExists($connection)) { + $i++; + $this->dbUser = $dbUser . $i; + }; + + // create the user + $query = $connection->prepare("CREATE USER " . addslashes($this->dbUser) . " CREATEDB PASSWORD '" . addslashes($this->dbPassword) . "'"); $query->execute(); } catch (DatabaseException $e) { $this->logger->error('Error while trying to create database user'); diff --git a/lib/private/Setup/Sqlite.php b/lib/private/Setup/Sqlite.php index 63b970be42e..4d860103b60 100644 --- a/lib/private/Setup/Sqlite.php +++ b/lib/private/Setup/Sqlite.php @@ -33,7 +33,7 @@ class Sqlite extends AbstractDatabase { } public function setupDatabase($username) { - $datadir = \OC::$server->getSystemConfig()->getValue('datadirectory'); + $datadir = \OC::$server->getSystemConfig()->getValue('datadirectory', \OC::$SERVERROOT . '/data'); //delete the old sqlite database first, might cause infinte loops otherwise if(file_exists("$datadir/owncloud.db")) { diff --git a/lib/private/Share20/DefaultShareProvider.php b/lib/private/Share20/DefaultShareProvider.php index 7a602950171..23e8db34d7f 100644 --- a/lib/private/Share20/DefaultShareProvider.php +++ b/lib/private/Share20/DefaultShareProvider.php @@ -162,7 +162,6 @@ class DefaultShareProvider implements IShareProvider { $this->dbConn->beginTransaction(); $qb->execute(); $id = $this->dbConn->lastInsertId('*PREFIX*share'); - $this->dbConn->commit(); // Now fetch the inserted share and create a complete share object $qb = $this->dbConn->getQueryBuilder(); @@ -172,6 +171,7 @@ class DefaultShareProvider implements IShareProvider { $cursor = $qb->execute(); $data = $cursor->fetch(); + $this->dbConn->commit(); $cursor->closeCursor(); if ($data === false) { @@ -853,7 +853,6 @@ class DefaultShareProvider implements IShareProvider { $entryData['permissions'] = $entryData['f_permissions']; $entryData['parent'] = $entryData['f_parent'];; $share->setNodeCacheEntry(Cache::cacheEntryFromData($entryData, - $entryData['storage_string_id'], \OC::$server->getMimeTypeLoader())); } diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index cd1d52c3bbf..acc142f62be 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -587,7 +587,6 @@ class Manager implements IManager { $share->setPassword($this->hasher->hash($share->getPassword())); } } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_EMAIL) { - $this->linkCreateChecks($share); $share->setToken( $this->secureRandom->generate( \OC\Share\Constants::TOKEN_LENGTH, @@ -1055,8 +1054,10 @@ class Manager implements IManager { public function getShareByToken($token) { $share = null; try { - $provider = $this->factory->getProviderForType(\OCP\Share::SHARE_TYPE_LINK); - $share = $provider->getShareByToken($token); + if($this->shareApiAllowLinks()) { + $provider = $this->factory->getProviderForType(\OCP\Share::SHARE_TYPE_LINK); + $share = $provider->getShareByToken($token); + } } catch (ProviderException $e) { } catch (ShareNotFound $e) { } @@ -1072,7 +1073,7 @@ class Manager implements IManager { } } - // If it is not a link share try to fetch a federated share by token + // If it is not a link share try to fetch a mail share by token if ($share === null && $this->shareProviderExists(\OCP\Share::SHARE_TYPE_EMAIL)) { try { $provider = $this->factory->getProviderForType(\OCP\Share::SHARE_TYPE_EMAIL); diff --git a/lib/private/SubAdmin.php b/lib/private/SubAdmin.php index 9a2beed09fe..0650c7c6c01 100644 --- a/lib/private/SubAdmin.php +++ b/lib/private/SubAdmin.php @@ -188,7 +188,7 @@ class SubAdmin extends PublicEmitter { * @param IGroup $group * @return bool */ - public function isSubAdminofGroup(IUser $user, IGroup $group) { + public function isSubAdminOfGroup(IUser $user, IGroup $group) { $qb = $this->dbConn->getQueryBuilder(); /* diff --git a/lib/private/SystemConfig.php b/lib/private/SystemConfig.php index 1029a6619ff..e5f1adaf004 100644 --- a/lib/private/SystemConfig.php +++ b/lib/private/SystemConfig.php @@ -44,7 +44,6 @@ class SystemConfig { 'passwordsalt' => true, 'secret' => true, 'updater.secret' => true, - 'ldap_agent_password' => true, 'proxyuserpwd' => true, 'log.condition' => [ 'shared_secret' => true, diff --git a/lib/private/Template/CSSResourceLocator.php b/lib/private/Template/CSSResourceLocator.php index ffeaf765ff5..3a474a1ecfd 100644 --- a/lib/private/Template/CSSResourceLocator.php +++ b/lib/private/Template/CSSResourceLocator.php @@ -25,23 +25,46 @@ namespace OC\Template; +use OCP\ILogger; + class CSSResourceLocator extends ResourceLocator { + + /** @var SCSSCacher */ + protected $scssCacher; + + /** + * @param ILogger $logger + * @param string $theme + * @param array $core_map + * @param array $party_map + * @param SCSSCacher $scssCacher + */ + public function __construct(ILogger $logger, $theme, $core_map, $party_map, $scssCacher) { + $this->scssCacher = $scssCacher; + + parent::__construct($logger, $theme, $core_map, $party_map); + } + /** * @param string $style */ public function doFind($style) { + $app = substr($style, 0, strpos($style, '/')); if (strpos($style, '3rdparty') === 0 && $this->appendIfExist($this->thirdpartyroot, $style.'.css') + || $this->cacheAndAppendScssIfExist($this->serverroot, $style.'.scss', $app) + || $this->cacheAndAppendScssIfExist($this->serverroot, 'core/'.$style.'.scss') || $this->appendIfExist($this->serverroot, $style.'.css') || $this->appendIfExist($this->serverroot, 'core/'.$style.'.css') ) { return; } - $app = substr($style, 0, strpos($style, '/')); $style = substr($style, strpos($style, '/')+1); $app_path = \OC_App::getAppPath($app); $app_url = \OC_App::getAppWebPath($app); - $this->append($app_path, $style.'.css', $app_url); + if(!$this->cacheAndAppendScssIfExist($app_path, $style.'.scss', $app)) { + $this->append($app_path, $style.'.css', $app_url); + } } /** @@ -53,4 +76,29 @@ class CSSResourceLocator extends ResourceLocator { || $this->appendIfExist($this->serverroot, $theme_dir.$style.'.css') || $this->appendIfExist($this->serverroot, $theme_dir.'core/'.$style.'.css'); } + + /** + * cache and append the scss $file if exist at $root + * + * @param string $root path to check + * @param string $file the filename + * @return bool True if the resource was found and cached, false otherwise + */ + protected function cacheAndAppendScssIfExist($root, $file, $app = 'core') { + if (is_file($root.'/'.$file)) { + if($this->scssCacher !== null) { + if($this->scssCacher->process($root, $file, $app)) { + $this->append($root, $this->scssCacher->getCachedSCSS($app, $file), false); + return true; + } else { + $this->logger->warning('Failed to compile and/or save '.$root.'/'.$file, ['app' => 'core']); + return false; + } + } else { + $this->logger->debug('Scss is disabled for '.$root.'/'.$file.', ignoring', ['app' => 'core']); + return true; + } + } + return false; + } } diff --git a/lib/private/Template/ResourceLocator.php b/lib/private/Template/ResourceLocator.php index 420317d27ac..e22ebdcab7d 100644..100755 --- a/lib/private/Template/ResourceLocator.php +++ b/lib/private/Template/ResourceLocator.php @@ -75,7 +75,7 @@ abstract class ResourceLocator { $this->doFind($resource); } catch (ResourceNotFoundException $e) { $resourceApp = substr($resource, 0, strpos($resource, '/')); - $this->logger->error('Could not find resource file "' . $e->getResourcePath() . '"', ['app' => $resourceApp]); + $this->logger->debug('Could not find resource file "' . $e->getResourcePath() . '"', ['app' => $resourceApp]); } } if (!empty($this->theme)) { @@ -84,7 +84,7 @@ abstract class ResourceLocator { $this->doFindTheme($resource); } catch (ResourceNotFoundException $e) { $resourceApp = substr($resource, 0, strpos($resource, '/')); - $this->logger->error('Could not find resource file "' . $e->getResourcePath() . '"', ['app' => $resourceApp]); + $this->logger->debug('Could not find resource file "' . $e->getResourcePath() . '"', ['app' => $resourceApp]); } } } diff --git a/lib/private/Template/SCSSCacher.php b/lib/private/Template/SCSSCacher.php new file mode 100644 index 00000000000..d6f5a2c6fd3 --- /dev/null +++ b/lib/private/Template/SCSSCacher.php @@ -0,0 +1,190 @@ +<?php +/** + * @copyright Copyright (c) 2016, John Molakvoæ (skjnldsv@protonmail.com) + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Template; + +use Leafo\ScssPhp\Compiler; +use Leafo\ScssPhp\Exception\ParserException; +use Leafo\ScssPhp\Formatter\Crunched; +use Leafo\ScssPhp\Formatter\Expanded; +use OC\SystemConfig; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; +use OCP\Files\SimpleFS\ISimpleFolder; +use OCP\ILogger; +use OCP\IURLGenerator; + +class SCSSCacher { + + /** @var ILogger */ + protected $logger; + + /** @var IAppData */ + protected $appData; + + /** @var IURLGenerator */ + protected $urlGenerator; + + /** @var SystemConfig */ + protected $systemConfig; + + /** + * @param ILogger $logger + * @param IAppData $appData + * @param IURLGenerator $urlGenerator + * @param SystemConfig $systemConfig + */ + public function __construct(ILogger $logger, IAppData $appData, IURLGenerator $urlGenerator, SystemConfig $systemConfig) { + $this->logger = $logger; + $this->appData = $appData; + $this->urlGenerator = $urlGenerator; + $this->systemConfig = $systemConfig; + } + + /** + * Process the caching process if needed + * @param string $root Root path to the nextcloud installation + * @param string $file + * @param string $app The app name + * @return boolean + */ + public function process($root, $file, $app) { + $path = explode('/', $root . '/' . $file); + + $fileNameSCSS = array_pop($path); + $fileNameCSS = str_replace('.scss', '.css', $fileNameSCSS); + + $path = implode('/', $path); + + $webDir = explode('/', $file); + array_pop($webDir); + $webDir = implode('/', $webDir); + + try { + $folder = $this->appData->getFolder($app); + } catch(NotFoundException $e) { + // creating css appdata folder + $folder = $this->appData->newFolder($app); + } + + if($this->isCached($fileNameCSS, $fileNameSCSS, $folder, $path)) { + return true; + } else { + return $this->cache($path, $fileNameCSS, $fileNameSCSS, $folder, $webDir); + } + } + + /** + * Check if the file is cached or not + * @param string $fileNameCSS + * @param string $fileNameSCSS + * @param ISimpleFolder $folder + * @param string $path + * @return boolean + */ + private function isCached($fileNameCSS, $fileNameSCSS, ISimpleFolder $folder, $path) { + try{ + $cachedFile = $folder->getFile($fileNameCSS); + if( $cachedFile->getMTime() > filemtime($path.'/'.$fileNameSCSS) + && $cachedFile->getSize() > 0 ) { + return true; + } + } catch(NotFoundException $e) { + return false; + } + return false; + } + + /** + * Cache the file with AppData + * @param string $path + * @param string $fileNameCSS + * @param string $fileNameSCSS + * @param ISimpleFolder $folder + * @param string $webDir + * @return boolean + */ + private function cache($path, $fileNameCSS, $fileNameSCSS, ISimpleFolder $folder, $webDir) { + $scss = new Compiler(); + $scss->setImportPaths($path); + if($this->systemConfig->getValue('debug')) { + // Debug mode + $scss->setFormatter(Expanded::class); + $scss->setLineNumberStyle(Compiler::LINE_COMMENTS); + } else { + // Compression + $scss->setFormatter(Crunched::class); + } + + try { + $cachedfile = $folder->getFile($fileNameCSS); + } catch(NotFoundException $e) { + $cachedfile = $folder->newFile($fileNameCSS); + } + + // Compile + try { + $compiledScss = $scss->compile('@import "'.$fileNameSCSS.'";'); + } catch(ParserException $e) { + $this->logger->error($e, ['app' => 'core']); + return false; + } + + try { + $cachedfile->putContent($this->rebaseUrls($compiledScss, $webDir)); + $this->logger->debug($webDir.'/'.$fileNameSCSS.' compiled and successfully cached', ['app' => 'core']); + return true; + } catch(NotFoundException $e) { + return false; + } + } + + /** + * Add the correct uri prefix to make uri valid again + * @param string $css + * @param string $webDir + * @return string + */ + private function rebaseUrls($css, $webDir) { + $re = '/url\([\'"]([\.\w?=\/-]*)[\'"]\)/x'; + // OC\Route\Router:75 + if(($this->systemConfig->getValue('htaccess.IgnoreFrontController', false) === true || getenv('front_controller_active') === 'true')) { + $subst = 'url(\'../../'.$webDir.'/$1\')'; + } else { + $subst = 'url(\'../../../'.$webDir.'/$1\')'; + } + return preg_replace($re, $subst, $css); + } + + /** + * Return the cached css file uri + * @param string $appName the app name + * @param string $fileName + * @return string + */ + public function getCachedSCSS($appName, $fileName) { + $tmpfileLoc = explode('/', $fileName); + $fileName = array_pop($tmpfileLoc); + $fileName = str_replace('.scss', '.css', $fileName); + + return substr($this->urlGenerator->linkToRoute('core.Css.getCss', array('fileName' => $fileName, 'appName' => $appName)), strlen(\OC::$WEBROOT) + 1); + } +} diff --git a/lib/private/TemplateLayout.php b/lib/private/TemplateLayout.php index 8919f14216e..7ded109f76b 100644 --- a/lib/private/TemplateLayout.php +++ b/lib/private/TemplateLayout.php @@ -35,15 +35,8 @@ */ namespace OC; -use Assetic\Asset\AssetCollection; -use Assetic\Asset\FileAsset; -use Assetic\AssetWriter; -use Assetic\Filter\CssImportFilter; -use Assetic\Filter\CssMinFilter; -use Assetic\Filter\CssRewriteFilter; -use Assetic\Filter\JSqueezeFilter; -use Assetic\Filter\SeparatorFilter; use OC\Template\JSConfigHelper; +use OC\Template\SCSSCacher; class TemplateLayout extends \OC_Template { @@ -164,11 +157,18 @@ class TemplateLayout extends \OC_Template { foreach($jsFiles as $info) { $web = $info[1]; $file = $info[2]; - $this->append( 'jsfiles', $web.'/'.$file . '?v=' . self::$versionHash); + $this->append( 'jsfiles', $web.'/'.$file . $this->getVersionHashSuffix() ); } - // Add the css files - $cssFiles = self::findStylesheetFiles(\OC_Util::$styles); + // Do not initialise scss appdata until we have a fully installed instance + // Do not load scss for update, errors, installation or login page + if(\OC::$server->getSystemConfig()->getValue('installed', false) + && !\OCP\Util::needUpgrade() + && strpos(\OC::$server->getRequest()->getRequestUri(), \OC::$server->getURLGenerator()->linkToRoute('core.login.tryLogin')) !== 0) { + $cssFiles = self::findStylesheetFiles(\OC_Util::$styles); + } else { + $cssFiles = self::findStylesheetFiles(\OC_Util::$styles, false); + } $this->assign('cssfiles', array()); $this->assign('printcssfiles', []); $this->assign('versionHash', self::$versionHash); @@ -177,26 +177,47 @@ class TemplateLayout extends \OC_Template { $file = $info[2]; if (substr($file, -strlen('print.css')) === 'print.css') { - $this->append( 'printcssfiles', $web.'/'.$file . '?v=' . self::$versionHash); + $this->append( 'printcssfiles', $web.'/'.$file . $this->getVersionHashSuffix() ); } else { - $this->append( 'cssfiles', $web.'/'.$file . '?v=' . self::$versionHash); + $this->append( 'cssfiles', $web.'/'.$file . $this->getVersionHashSuffix() ); } } } + protected function getVersionHashSuffix() { + if(\OC::$server->getConfig()->getSystemValue('debug', false)) { + // allows chrome workspace mapping in debug mode + return ""; + } + + return '?v=' . self::$versionHash; + } + /** * @param array $styles * @return array */ - static public function findStylesheetFiles($styles) { + static public function findStylesheetFiles($styles, $compileScss = true) { // Read the selected theme from the config file $theme = \OC_Util::getTheme(); + if($compileScss) { + $SCSSCacher = new SCSSCacher( + \OC::$server->getLogger(), + \OC::$server->getAppDataDir('css'), + \OC::$server->getURLGenerator(), + \OC::$server->getSystemConfig() + ); + } else { + $SCSSCacher = null; + } + $locator = new \OC\Template\CSSResourceLocator( \OC::$server->getLogger(), $theme, array( \OC::$SERVERROOT => \OC::$WEBROOT ), - array( \OC::$SERVERROOT => \OC::$WEBROOT )); + array( \OC::$SERVERROOT => \OC::$WEBROOT ), + $SCSSCacher); $locator->find($styles); return $locator->getResources(); } diff --git a/lib/private/Updater.php b/lib/private/Updater.php index e30777227cc..a66d49941cd 100644 --- a/lib/private/Updater.php +++ b/lib/private/Updater.php @@ -82,6 +82,12 @@ class Updater extends BasicEmitter { $this->log = $log; $this->config = $config; $this->checker = $checker; + + // If at least PHP 7.0.0 is used we don't need to disable apps as we catch + // fatal errors and exceptions and disable the app just instead. + if(version_compare(phpversion(), '7.0.0', '>=')) { + $this->skip3rdPartyAppsDisable = true; + } } /** diff --git a/lib/private/User/Database.php b/lib/private/User/Database.php index 28cb3302858..a281572ad55 100644 --- a/lib/private/User/Database.php +++ b/lib/private/User/Database.php @@ -238,6 +238,12 @@ class Database extends Backend implements IUserBackend { */ private function loadUser($uid) { if (!isset($this->cache[$uid])) { + //guests $uid could be NULL or '' + if ($uid === null || $uid === '') { + $this->cache[$uid]=false; + return true; + } + $query = \OC_DB::prepare('SELECT `uid`, `displayname` FROM `*PREFIX*users` WHERE LOWER(`uid`) = LOWER(?)'); $result = $query->execute(array($uid)); diff --git a/lib/private/User/Session.php b/lib/private/User/Session.php index 36bd45521cf..2ebe895a592 100644 --- a/lib/private/User/Session.php +++ b/lib/private/User/Session.php @@ -317,7 +317,7 @@ class Session implements IUserSession, Emitter { $password, IRequest $request, OC\Security\Bruteforce\Throttler $throttler) { - $currentDelay = $throttler->sleepDelay($request->getRemoteAddress()); + $currentDelay = $throttler->sleepDelay($request->getRemoteAddress(), 'login'); if ($this->manager instanceof PublicEmitter) { $this->manager->emit('\OC\User', 'preLogin', array($user, $password)); @@ -338,7 +338,7 @@ class Session implements IUserSession, Emitter { $throttler->registerAttempt('login', $request->getRemoteAddress(), ['uid' => $user]); if($currentDelay === 0) { - $throttler->sleepDelay($request->getRemoteAddress()); + $throttler->sleepDelay($request->getRemoteAddress(), 'login'); } return false; } @@ -744,6 +744,7 @@ class Session implements IUserSession, Emitter { //login $this->setUser($user); + $this->setLoginName($this->tokenProvider->getToken($sessionId)->getLoginName()); $user->updateLastLoginTimestamp(); $this->manager->emit('\OC\User', 'postRememberedLogin', [$user]); return true; @@ -768,7 +769,7 @@ class Session implements IUserSession, Emitter { try { $this->tokenProvider->invalidateToken($this->session->getId()); } catch (SessionNotAvailableException $ex) { - + } } $this->setUser(null); diff --git a/lib/private/User/User.php b/lib/private/User/User.php index 3cc6dc3b7ed..c37bb59028e 100644 --- a/lib/private/User/User.php +++ b/lib/private/User/User.php @@ -197,6 +197,8 @@ class User implements IUser { if ($this->emitter) { $this->emitter->emit('\OC\User', 'preDelete', array($this)); } + // get the home now because it won't return it after user deletion + $homePath = $this->getHome(); $result = $this->backend->deleteUser($this->uid); if ($result) { @@ -210,7 +212,11 @@ class User implements IUser { \OC::$server->getConfig()->deleteAllUserValues($this->uid); // Delete user files in /data/ - \OC_Helper::rmdirr($this->getHome()); + if ($homePath !== false) { + // FIXME: this operates directly on FS, should use View instead... + // also this is not testable/mockable... + \OC_Helper::rmdirr($homePath); + } // Delete the users entry in the storage table Storage::remove('home::' . $this->uid); @@ -261,7 +267,7 @@ class User implements IUser { if ($this->backend->implementsActions(Backend::GET_HOME) and $home = $this->backend->getHome($this->uid)) { $this->home = $home; } elseif ($this->config) { - $this->home = $this->config->getSystemValue('datadirectory') . '/' . $this->uid; + $this->home = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . '/' . $this->uid; } else { $this->home = \OC::$SERVERROOT . '/data/' . $this->uid; } diff --git a/lib/private/legacy/app.php b/lib/private/legacy/app.php index adf29601ac6..a475b895d9f 100644 --- a/lib/private/legacy/app.php +++ b/lib/private/legacy/app.php @@ -273,9 +273,17 @@ class OC_App { $appTypes = implode(',', $appData['types']); } else { $appTypes = ''; + $appData['types'] = []; } \OC::$server->getAppConfig()->setValue($app, 'types', $appTypes); + + if (\OC::$server->getAppManager()->hasProtectedAppType($appData['types'])) { + $enabled = \OC::$server->getAppConfig()->getValue($app, 'enabled', 'yes'); + if ($enabled !== 'yes' && $enabled !== 'no') { + \OC::$server->getAppConfig()->setValue($app, 'enabled', 'yes'); + } + } } /** @@ -1296,43 +1304,21 @@ class OC_App { $data['summary'] = self::findBestL10NOption($data['summary'], $lang); } if ($lang && isset($data['description']) && is_array($data['description'])) { - $data['description'] = self::findBestL10NOption($data['description'], $lang); - } - - // 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'] = ''; - } + $data['description'] = trim(self::findBestL10NOption($data['description'], $lang)); + } else if (isset($data['description']) && is_string($data['description'])) { + $data['description'] = trim($data['description']); + } else { + $data['description'] = ''; } return $data; } /** - * @param $config - * @param $l - * @param $info - * @throws Exception + * @param \OCP\IConfig $config + * @param \OCP\IL10N $l + * @param array $info + * @throws \Exception */ protected static function checkAppDependencies($config, $l, $info) { $dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l); diff --git a/lib/private/legacy/files.php b/lib/private/legacy/files.php index b6c6857a1bf..8c9adad0d49 100644 --- a/lib/private/legacy/files.php +++ b/lib/private/legacy/files.php @@ -148,6 +148,7 @@ class OC_Files { $streamer->sendHeaders($name); $executionTime = intval(OC::$server->getIniWrapper()->getNumeric('max_execution_time')); set_time_limit(0); + ignore_user_abort(true); if ($getType === self::ZIP_FILES) { foreach ($files as $file) { $file = $dir . '/' . $file; diff --git a/lib/private/legacy/helper.php b/lib/private/legacy/helper.php index 9c4bc895fb9..dfe2e09afff 100644 --- a/lib/private/legacy/helper.php +++ b/lib/private/legacy/helper.php @@ -52,6 +52,7 @@ class OC_Helper { /** * Creates an absolute url for public use + * * @param string $service id * @param bool $add_slash * @return string the url @@ -62,13 +63,14 @@ class OC_Helper { if ($service === 'files') { $url = OC::$server->getURLGenerator()->getAbsoluteURL('/s'); } else { - $url = OC::$server->getURLGenerator()->getAbsoluteURL(OC::$server->getURLGenerator()->linkTo('', 'public.php').'?service='.$service); + $url = OC::$server->getURLGenerator()->getAbsoluteURL(OC::$server->getURLGenerator()->linkTo('', 'public.php') . '?service=' . $service); } return $url . (($add_slash && $service[strlen($service) - 1] != '/') ? '/' : ''); } /** * Make a human file size + * * @param int $bytes file size in bytes * @return string a human readable file size * @@ -104,6 +106,7 @@ class OC_Helper { /** * Make a php file size + * * @param int $bytes file size in bytes * @return string a php parseable file size * @@ -130,6 +133,7 @@ class OC_Helper { /** * Make a computer file size + * * @param string $str file size in human readable format * @return float a file size in bytes * @@ -172,6 +176,7 @@ class OC_Helper { /** * Recursive copying of folders + * * @param string $src source folder * @param string $dest target folder * @@ -194,6 +199,7 @@ class OC_Helper { /** * 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 @@ -393,6 +399,7 @@ class OC_Helper { /** * 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 @@ -425,7 +432,7 @@ class OC_Helper { * @return int number of bytes representing */ public static function maxUploadFilesize($dir, $freeSpace = null) { - if (is_null($freeSpace) || $freeSpace < 0){ + if (is_null($freeSpace) || $freeSpace < 0) { $freeSpace = self::freeSpace($dir); } return min($freeSpace, self::uploadLimit()); @@ -443,7 +450,7 @@ class OC_Helper { $freeSpace = max($freeSpace, 0); return $freeSpace; } else { - return (INF > 0)? INF: PHP_INT_MAX; // work around https://bugs.php.net/bug.php?id=69188 + return (INF > 0) ? INF : PHP_INT_MAX; // work around https://bugs.php.net/bug.php?id=69188 } } @@ -510,7 +517,7 @@ class OC_Helper { if (empty($paths)) { $paths = '/usr/local/bin /usr/bin /opt/bin /bin'; } else { - $paths = str_replace(':',' ',getenv('PATH')); + $paths = str_replace(':', ' ', getenv('PATH')); } $command = 'find ' . $paths . ' -name ' . escapeshellarg($program) . ' 2> /dev/null'; exec($command, $output, $returnCode); @@ -533,6 +540,12 @@ class OC_Helper { * @throws \OCP\Files\NotFoundException */ public static function getStorageInfo($path, $rootInfo = null) { + $memcache = \OC::$server->getMemCacheFactory()->create('storageInfo'); + $cacheKey = $rootInfo ? '__root__' . md5($path) : md5($path); + $cached = $memcache->get($cacheKey); + if (is_array($cached)) { + return $cached; + } // return storage info without adding mount points $includeExtStorage = \OC::$server->getSystemConfig()->getValue('quota_include_external_storage', false); @@ -597,10 +610,20 @@ class OC_Helper { $ownerId = $storage->getOwner($path); $ownerDisplayName = ''; $owner = \OC::$server->getUserManager()->get($ownerId); - if($owner) { + if ($owner) { $ownerDisplayName = $owner->getDisplayName(); } + $memcache->set($cacheKey, [ + 'free' => $free, + 'used' => $used, + 'quota' => $quota, + 'total' => $total, + 'relative' => $relative, + 'owner' => $ownerId, + 'ownerDisplayName' => $ownerDisplayName, + ], 5 * 60); + return [ 'free' => $free, 'used' => $used, @@ -645,6 +668,7 @@ class OC_Helper { /** * Returns whether the config file is set manually to read-only + * * @return bool */ public static function isReadOnlyConfigEnabled() { diff --git a/lib/private/legacy/user.php b/lib/private/legacy/user.php index ed0d14a1ab9..0a52be7565d 100644 --- a/lib/private/legacy/user.php +++ b/lib/private/legacy/user.php @@ -125,9 +125,16 @@ class OC_User { * setup the configured backends in config.php */ public static function setupBackends() { - OC_App::loadApps(array('prelogin')); - $backends = \OC::$server->getSystemConfig()->getValue('user_backends', array()); + OC_App::loadApps(['prelogin']); + $backends = \OC::$server->getSystemConfig()->getValue('user_backends', []); + if (isset($backends['default']) && !$backends['default']) { + // clear default backends + self::clearBackends(); + } foreach ($backends as $i => $config) { + if (!is_array($config)) { + continue; + } $class = $config['class']; $arguments = $config['arguments']; if (class_exists($class)) { diff --git a/lib/private/legacy/util.php b/lib/private/legacy/util.php index 55dc5ae7c15..5ef1130d361 100644 --- a/lib/private/legacy/util.php +++ b/lib/private/legacy/util.php @@ -1002,27 +1002,6 @@ class OC_Util { } /** - * 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 @@ -1381,12 +1360,12 @@ class OC_Util { } /** - * A human readable string is generated based on version, channel and build number + * A human readable string is generated based on version and build number * * @return string */ public static function getHumanVersion() { - $version = OC_Util::getVersionString() . ' (' . OC_Util::getChannel() . ')'; + $version = OC_Util::getVersionString(); $build = OC_Util::getBuild(); if (!empty($build) and OC_Util::getChannel() === 'daily') { $version .= ' Build:' . $build; |