diff options
230 files changed, 9886 insertions, 2773 deletions
diff --git a/apps/encryption/appinfo/app.php b/apps/encryption/appinfo/app.php index 240a1726715..6bbf2113366 100644 --- a/apps/encryption/appinfo/app.php +++ b/apps/encryption/appinfo/app.php @@ -1,7 +1,9 @@ <?php /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 2/19/15, 9:52 AM + * @author Björn Schießle <schiessle@owncloud.com> + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * diff --git a/apps/encryption/appinfo/application.php b/apps/encryption/appinfo/application.php index 0d1bd0d6bed..34845ecf1e8 100644 --- a/apps/encryption/appinfo/application.php +++ b/apps/encryption/appinfo/application.php @@ -1,7 +1,10 @@ <?php /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 3/11/15, 11:03 AM + * @author Björn Schießle <schiessle@owncloud.com> + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Lukas Reschke <lukas@owncloud.com> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * @@ -47,7 +50,6 @@ class Application extends \OCP\AppFramework\App { private $config; /** - * @param $appName * @param array $urlParams */ public function __construct($urlParams = array()) { @@ -57,9 +59,6 @@ class Application extends \OCP\AppFramework\App { $this->registerServices(); } - /** - * - */ public function registerHooks() { if (!$this->config->getSystemValue('maintenance', false)) { @@ -87,9 +86,6 @@ class Application extends \OCP\AppFramework\App { } } - /** - * - */ public function registerEncryptionModule() { $container = $this->getContainer(); $container->registerService('EncryptionModule', function (IAppContainer $c) { @@ -102,9 +98,6 @@ class Application extends \OCP\AppFramework\App { $this->encryptionManager->registerEncryptionModule($module); } - /** - * - */ public function registerServices() { $container = $this->getContainer(); @@ -130,7 +123,6 @@ class Application extends \OCP\AppFramework\App { ); }); - $container->registerService('Recovery', function (IAppContainer $c) { $server = $c->getServer(); @@ -179,9 +171,6 @@ class Application extends \OCP\AppFramework\App { } - /** - * - */ public function registerSettings() { // Register settings scripts App::registerAdmin('encryption', 'settings/settings-admin'); diff --git a/apps/encryption/appinfo/routes.php b/apps/encryption/appinfo/routes.php index d4867f5fdaa..0dab4a01b97 100644 --- a/apps/encryption/appinfo/routes.php +++ b/apps/encryption/appinfo/routes.php @@ -1,7 +1,8 @@ <?php /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 2/19/15, 11:22 AM + * @author Björn Schießle <schiessle@owncloud.com> + * @author Clark Tomlinson <fallen013@gmail.com> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * diff --git a/apps/encryption/controller/recoverycontroller.php b/apps/encryption/controller/recoverycontroller.php index da55d81f63a..9c07bda62e4 100644 --- a/apps/encryption/controller/recoverycontroller.php +++ b/apps/encryption/controller/recoverycontroller.php @@ -1,7 +1,9 @@ <?php /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 2/19/15, 11:25 AM + * @author Björn Schießle <schiessle@owncloud.com> + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Lukas Reschke <lukas@owncloud.com> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * @@ -27,7 +29,6 @@ use OCP\AppFramework\Controller; use OCP\IConfig; use OCP\IL10N; use OCP\IRequest; -use OCP\JSON; use OCP\AppFramework\Http\DataResponse; class RecoveryController extends Controller { @@ -51,13 +52,23 @@ class RecoveryController extends Controller { * @param IL10N $l10n * @param Recovery $recovery */ - public function __construct($AppName, IRequest $request, IConfig $config, IL10N $l10n, Recovery $recovery) { + public function __construct($AppName, + IRequest $request, + IConfig $config, + IL10N $l10n, + Recovery $recovery) { parent::__construct($AppName, $request); $this->config = $config; $this->l = $l10n; $this->recovery = $recovery; } + /** + * @param string $recoveryPassword + * @param string $confirmPassword + * @param string $adminEnableRecovery + * @return DataResponse + */ public function adminRecovery($recoveryPassword, $confirmPassword, $adminEnableRecovery) { // Check if both passwords are the same if (empty($recoveryPassword)) { @@ -88,6 +99,12 @@ class RecoveryController extends Controller { } } + /** + * @param string $newPassword + * @param string $oldPassword + * @param string $confirmPassword + * @return DataResponse + */ public function changeRecoveryPassword($newPassword, $oldPassword, $confirmPassword) { //check if both passwords are the same if (empty($oldPassword)) { @@ -132,6 +149,9 @@ class RecoveryController extends Controller { /** * @NoAdminRequired + * + * @param string $userEnableRecovery + * @return DataResponse */ public function userSetRecovery($userEnableRecovery) { if ($userEnableRecovery === '0' || $userEnableRecovery === '1') { diff --git a/apps/encryption/hooks/contracts/ihook.php b/apps/encryption/hooks/contracts/ihook.php index 2cc01fd7c9b..d4f20700d78 100644 --- a/apps/encryption/hooks/contracts/ihook.php +++ b/apps/encryption/hooks/contracts/ihook.php @@ -1,7 +1,7 @@ <?php /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 2/19/15, 10:03 AM + * @author Clark Tomlinson <fallen013@gmail.com> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * diff --git a/apps/encryption/hooks/userhooks.php b/apps/encryption/hooks/userhooks.php index 1ec0950d941..c39fa34108b 100644 --- a/apps/encryption/hooks/userhooks.php +++ b/apps/encryption/hooks/userhooks.php @@ -1,7 +1,9 @@ <?php /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 2/19/15, 10:02 AM + * @author Björn Schießle <schiessle@owncloud.com> + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * diff --git a/apps/encryption/js/detect-migration.js b/apps/encryption/js/detect-migration.js deleted file mode 100644 index f5627edf4e4..00000000000 --- a/apps/encryption/js/detect-migration.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2013 - * Bjoern Schiessle <schiessle@owncloud.com> - * This file is licensed under the Affero General Public License version 3 or later. - * See the COPYING-README file. - */ - - -$(document).ready(function(){ - $('form[name="login"]').on('submit', function() { - var user = $('#user').val(); - var password = $('#password').val(); - $.ajax({ - type: 'POST', - url: OC.linkTo('files_encryption', 'ajax/getMigrationStatus.php'), - dataType: 'json', - data: {user: user, password: password}, - async: false, - success: function(response) { - if (response.data.migrationStatus === OC.Encryption.MIGRATION_OPEN) { - var message = t('files_encryption', 'Initial encryption started... This can take some time. Please wait.'); - $('#messageText').text(message); - $('#message').removeClass('hidden').addClass('update'); - } else if (response.data.migrationStatus === OC.Encryption.MIGRATION_IN_PROGRESS) { - var message = t('files_encryption', 'Initial encryption running... Please try again later.'); - $('#messageText').text(message); - $('#message').removeClass('hidden').addClass('update'); - } - } - }); - }); - -}); diff --git a/apps/encryption/lib/crypto/crypt.php b/apps/encryption/lib/crypto/crypt.php index c0b737a3daa..974e0038afc 100644 --- a/apps/encryption/lib/crypto/crypt.php +++ b/apps/encryption/lib/crypto/crypt.php @@ -1,7 +1,9 @@ <?php /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 2/19/15, 1:42 PM + * @author Björn Schießle <schiessle@owncloud.com> + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * @@ -35,6 +37,8 @@ use OCP\IUserSession; class Crypt { const DEFAULT_CIPHER = 'AES-256-CFB'; + // default cipher from old ownCloud versions + const LEGACY_CIPHER = 'AES-128-CFB'; const HEADER_START = 'HBEGIN'; const HEADER_END = 'HEND'; @@ -71,7 +75,7 @@ class Crypt { $res = $this->getOpenSSLPKey(); if (!$res) { - $log->error("Encryption Library could'nt generate users key-pair for {$this->user->getUID()}", + $log->error("Encryption Library couldn't generate users key-pair for {$this->user->getUID()}", ['app' => 'encryption']); if (openssl_error_string()) { @@ -90,7 +94,7 @@ class Crypt { 'privateKey' => $privateKey ]; } - $log->error('Encryption library couldn\'t export users private key, please check your servers openSSL configuration.' . $this->user->getUID(), + $log->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user->getUID(), ['app' => 'encryption']); if (openssl_error_string()) { $log->error('Encryption Library:' . openssl_error_string(), @@ -147,6 +151,16 @@ class Crypt { } /** + * generate header for encrypted file + */ + public function generateHeader() { + $cipher = $this->getCipher(); + $header = self::HEADER_START . ':cipher:' . $cipher . ':' . self::HEADER_END; + + return $header; + } + + /** * @param string $plainContent * @param string $iv * @param string $passPhrase @@ -203,23 +217,28 @@ class Crypt { } /** - * @param string $recoveryKey + * @param string $privateKey * @param string $password * @return bool|string */ - public function decryptPrivateKey($recoveryKey, $password) { + public function decryptPrivateKey($privateKey, $password) { - $header = $this->parseHeader($recoveryKey); - $cipher = $this->getCipher(); + $header = $this->parseHeader($privateKey); + + if (isset($header['cipher'])) { + $cipher = $header['cipher']; + } else { + $cipher = self::LEGACY_CIPHER; + } // If we found a header we need to remove it from the key we want to decrypt if (!empty($header)) { - $recoveryKey = substr($recoveryKey, - strpos($recoveryKey, + $privateKey = substr($privateKey, + strpos($privateKey, self::HEADER_END) + strlen(self::HEADER_START)); } - $plainKey = $this->symmetricDecryptFileContent($recoveryKey, + $plainKey = $this->symmetricDecryptFileContent($privateKey, $password, $cipher); @@ -360,8 +379,11 @@ class Crypt { } /** - * Generate a pseudo random 256-bit ASCII key, used as file key + * Generate a cryptographically secure pseudo-random base64 encoded 256-bit + * ASCII key, used as file key + * * @return string + * @throws \Exception */ public static function generateFileKey() { // Generate key @@ -419,7 +441,7 @@ class Crypt { } /** - * @param $plainContent + * @param string $plainContent * @param array $keyFiles * @return array * @throws MultiKeyEncryptException diff --git a/apps/encryption/lib/crypto/encryption.php b/apps/encryption/lib/crypto/encryption.php index 7c633b7411f..13beda196ce 100644 --- a/apps/encryption/lib/crypto/encryption.php +++ b/apps/encryption/lib/crypto/encryption.php @@ -1,9 +1,24 @@ <?php /** - * @author Clark Tomlinson <fallen013@gmail.com> - * @since 3/6/15, 2:28 PM - * @link http:/www.clarkt.com - * @copyright Clark Tomlinson © 2015 + * @author Björn Schießle <schiessle@owncloud.com> + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Lukas Reschke <lukas@owncloud.com> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> * */ @@ -92,7 +107,7 @@ class Encryption implements IEncryptionModule { * written to the header, in case of a write operation * or if no additional data is needed return a empty array */ - public function begin($path, $user, $header, $accessList) { + public function begin($path, $user, array $header, array $accessList) { if (isset($header['cipher'])) { $this->cipher = $header['cipher']; @@ -231,7 +246,7 @@ class Encryption implements IEncryptionModule { * @param array $accessList who has access to the file contains the key 'users' and 'public' * @return boolean */ - public function update($path, $uid, $accessList) { + public function update($path, $uid, array $accessList) { $fileKey = $this->keyManager->getFileKey($path, $uid); $publicKeys = array(); foreach ($accessList['users'] as $user) { @@ -262,12 +277,11 @@ class Encryption implements IEncryptionModule { } if ($this->keyManager->recoveryKeyExists() && - $this->util->recoveryEnabled($this->user)) { + $this->util->isRecoveryEnabledForUser()) { $publicKeys[$this->keyManager->getRecoveryKeyId()] = $this->keyManager->getRecoveryKey(); } - return $publicKeys; } @@ -314,6 +328,10 @@ class Encryption implements IEncryptionModule { return 6126; } + /** + * @param string $path + * @return string + */ protected function getPathToRealFile($path) { $realPath = $path; $parts = explode('/', $path); diff --git a/apps/encryption/lib/exceptions/multikeydecryptexception.php b/apps/encryption/lib/exceptions/multikeydecryptexception.php index 1466d35eda3..48b916ff1b8 100644 --- a/apps/encryption/lib/exceptions/multikeydecryptexception.php +++ b/apps/encryption/lib/exceptions/multikeydecryptexception.php @@ -1,5 +1,23 @@ <?php - +/** + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ namespace OCA\Encryption\Exceptions; use OCP\Encryption\Exceptions\GenericEncryptionException; diff --git a/apps/encryption/lib/exceptions/multikeyencryptexception.php b/apps/encryption/lib/exceptions/multikeyencryptexception.php index daf528e2cf7..197e06adbf3 100644 --- a/apps/encryption/lib/exceptions/multikeyencryptexception.php +++ b/apps/encryption/lib/exceptions/multikeyencryptexception.php @@ -1,5 +1,23 @@ <?php - +/** + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ namespace OCA\Encryption\Exceptions; use OCP\Encryption\Exceptions\GenericEncryptionException; diff --git a/apps/encryption/lib/exceptions/privatekeymissingexception.php b/apps/encryption/lib/exceptions/privatekeymissingexception.php index 50d75870b20..29db5a16641 100644 --- a/apps/encryption/lib/exceptions/privatekeymissingexception.php +++ b/apps/encryption/lib/exceptions/privatekeymissingexception.php @@ -1,7 +1,9 @@ <?php - /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 2/25/15, 9:39 AM +/** + * @author Björn Schießle <schiessle@owncloud.com> + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * diff --git a/apps/encryption/lib/exceptions/publickeymissingexception.php b/apps/encryption/lib/exceptions/publickeymissingexception.php index 9638c28e427..078add0369a 100644 --- a/apps/encryption/lib/exceptions/publickeymissingexception.php +++ b/apps/encryption/lib/exceptions/publickeymissingexception.php @@ -1,6 +1,23 @@ <?php - - +/** + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ namespace OCA\Encryption\Exceptions; use OCP\Encryption\Exceptions\GenericEncryptionException; diff --git a/apps/encryption/lib/hookmanager.php b/apps/encryption/lib/hookmanager.php index 19ee142a622..4b885cd7f64 100644 --- a/apps/encryption/lib/hookmanager.php +++ b/apps/encryption/lib/hookmanager.php @@ -1,7 +1,9 @@ <?php /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 2/19/15, 10:13 AM + * @author Björn Schießle <schiessle@owncloud.com> + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Lukas Reschke <lukas@owncloud.com> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * @@ -48,9 +50,6 @@ class HookManager { return true; } - /** - * - */ public function fireHooks() { foreach ($this->hookInstances as $instance) { /** diff --git a/apps/encryption/lib/keymanager.php b/apps/encryption/lib/keymanager.php index 1f71a891e81..a280ea9bde3 100644 --- a/apps/encryption/lib/keymanager.php +++ b/apps/encryption/lib/keymanager.php @@ -1,5 +1,25 @@ <?php - +/** + * @author Björn Schießle <schiessle@owncloud.com> + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ namespace OCA\Encryption; use OC\Encryption\Exceptions\DecryptionFailedException; @@ -180,9 +200,10 @@ class KeyManager { $encryptedKey = $this->crypt->symmetricEncryptFileContent($keyPair['privateKey'], $password); + $header = $this->crypt->generateHeader(); if ($encryptedKey) { - $this->setPrivateKey($uid, $encryptedKey); + $this->setPrivateKey($uid, $header . $encryptedKey); return true; } return false; @@ -199,9 +220,10 @@ class KeyManager { $encryptedKey = $this->crypt->symmetricEncryptFileContent($keyPair['privateKey'], $password); + $header = $this->crypt->generateHeader(); if ($encryptedKey) { - $this->setSystemPrivateKey($this->getRecoveryKeyId(), $encryptedKey); + $this->setSystemPrivateKey($this->getRecoveryKeyId(), $header . $encryptedKey); return true; } return false; diff --git a/apps/encryption/lib/recovery.php b/apps/encryption/lib/recovery.php index 34acdd0a6e3..5c1e91866a0 100644 --- a/apps/encryption/lib/recovery.php +++ b/apps/encryption/lib/recovery.php @@ -1,7 +1,8 @@ <?php /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 2/19/15, 11:45 AM + * @author Björn Schießle <schiessle@owncloud.com> + * @author Clark Tomlinson <fallen013@gmail.com> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * @@ -128,6 +129,7 @@ class Recovery { * * @param string $newPassword * @param string $oldPassword + * @return bool */ public function changeRecoveryKeyPassword($newPassword, $oldPassword) { $recoveryKey = $this->keyManager->getSystemPrivateKey($this->keyManager->getRecoveryKeyId()); diff --git a/apps/encryption/lib/session.php b/apps/encryption/lib/session.php index e705611fa6e..85d2a7698ef 100644 --- a/apps/encryption/lib/session.php +++ b/apps/encryption/lib/session.php @@ -1,24 +1,24 @@ <?php - /** - * ownCloud - * - * @copyright (C) 2015 ownCloud, Inc. + * @author Björn Schießle <schiessle@owncloud.com> + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Lukas Reschke <lukas@owncloud.com> * - * @author Bjoern Schiessle <schiessle@owncloud.com> + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. + * 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 library is distributed in the hope that it will be useful, + * 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. + * 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/> * - * You should have received a copy of the GNU Affero General Public - * License along with this library. If not, see <http://www.gnu.org/licenses/>. */ namespace OCA\Encryption; @@ -34,6 +34,9 @@ class Session { const INIT_EXECUTED = '1'; const INIT_SUCCESSFUL = '2'; + /** + * @param ISession $session + */ public function __construct(ISession $session) { $this->session = $session; } diff --git a/apps/encryption/lib/users/setup.php b/apps/encryption/lib/users/setup.php index e80bf6003e6..2ec49b5c7fb 100644 --- a/apps/encryption/lib/users/setup.php +++ b/apps/encryption/lib/users/setup.php @@ -1,9 +1,23 @@ <?php /** - * @author Clark Tomlinson <fallen013@gmail.com> - * @since 3/6/15, 11:36 AM - * @link http:/www.clarkt.com - * @copyright Clark Tomlinson © 2015 + * @author Björn Schießle <schiessle@owncloud.com> + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Lukas Reschke <lukas@owncloud.com> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> * */ @@ -40,7 +54,10 @@ class Setup { * @param Crypt $crypt * @param KeyManager $keyManager */ - public function __construct(ILogger $logger, IUserSession $userSession, Crypt $crypt, KeyManager $keyManager) { + public function __construct(ILogger $logger, + IUserSession $userSession, + Crypt $crypt, + KeyManager $keyManager) { $this->logger = $logger; $this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : false; $this->crypt = $crypt; @@ -48,8 +65,8 @@ class Setup { } /** - * @param $uid userid - * @param $password user password + * @param string $uid userid + * @param string $password user password * @return bool */ public function setupUser($uid, $password) { @@ -57,8 +74,8 @@ class Setup { } /** - * @param $uid userid - * @param $password user password + * @param string $uid userid + * @param string $password user password * @return bool */ public function setupServerSide($uid, $password) { diff --git a/apps/encryption/lib/util.php b/apps/encryption/lib/util.php index 6b6b8b6b38c..04e04028caf 100644 --- a/apps/encryption/lib/util.php +++ b/apps/encryption/lib/util.php @@ -1,7 +1,8 @@ <?php /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 3/17/15, 10:31 AM + * @author Björn Schießle <schiessle@owncloud.com> + * @author Clark Tomlinson <fallen013@gmail.com> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * diff --git a/apps/encryption/settings/settings-admin.php b/apps/encryption/settings/settings-admin.php index 36e9c532bbd..c7ac8c09c6b 100644 --- a/apps/encryption/settings/settings-admin.php +++ b/apps/encryption/settings/settings-admin.php @@ -1,9 +1,24 @@ <?php /** - * Copyright (c) 2015 Clark Tomlinson <clark@owncloud.com> - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. + * @author Björn Schießle <schiessle@owncloud.com> + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * */ \OC_Util::checkAdminUser(); diff --git a/apps/encryption/settings/settings-personal.php b/apps/encryption/settings/settings-personal.php index ec3d30f457d..abbe62af615 100644 --- a/apps/encryption/settings/settings-personal.php +++ b/apps/encryption/settings/settings-personal.php @@ -1,9 +1,24 @@ <?php /** - * Copyright (c) 2015 Clark Tomlinson <clark@owncloud.com> - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. + * @author Björn Schießle <schiessle@owncloud.com> + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * */ $session = new \OCA\Encryption\Session(\OC::$server->getSession()); diff --git a/apps/encryption/tests/hooks/UserHooksTest.php b/apps/encryption/tests/hooks/UserHooksTest.php new file mode 100644 index 00000000000..1d76e3ba1a2 --- /dev/null +++ b/apps/encryption/tests/hooks/UserHooksTest.php @@ -0,0 +1,217 @@ +<?php +/** + * @author Clark Tomlinson <clark@owncloud.com> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + */ + + + +namespace OCA\Encryption\Tests\Hooks; + + +use OCA\Encryption\Hooks\UserHooks; +use Test\TestCase; + +class UserHooksTest extends TestCase { + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $utilMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $recoveryMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $sessionMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $keyManagerMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $userSetupMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $userSessionMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $cryptMock; + /** + * @var UserHooks + */ + private $instance; + + private $params = ['uid' => 'testUser', 'password' => 'password']; + + public function testLogin() { + $this->userSetupMock->expects($this->exactly(2)) + ->method('setupUser') + ->willReturnOnConsecutiveCalls(true, false); + + $this->keyManagerMock->expects($this->once()) + ->method('init') + ->with('testUser', 'password'); + + $this->assertNull($this->instance->login($this->params)); + $this->assertFalse($this->instance->login($this->params)); + } + + public function testLogout() { + $this->sessionMock->expects($this->once()) + ->method('clear'); + + $this->assertNull($this->instance->logout()); + } + + public function testPostCreateUser() { + $this->userSetupMock->expects($this->once()) + ->method('setupUser'); + + $this->assertNull($this->instance->postCreateUser($this->params)); + } + + public function testPostDeleteUser() { + $this->keyManagerMock->expects($this->once()) + ->method('deletePublicKey') + ->with('testUser'); + + $this->assertNull($this->instance->postDeleteUser($this->params)); + } + + public function testPreSetPassphrase() { + $this->userSessionMock->expects($this->once()) + ->method('canChangePassword'); + + $this->assertNull($this->instance->preSetPassphrase($this->params)); + } + + public function testSetPassphrase() { + $this->sessionMock->expects($this->exactly(4)) + ->method('getPrivateKey') + ->willReturnOnConsecutiveCalls(true, false); + + $this->cryptMock->expects($this->exactly(4)) + ->method('symmetricEncryptFileContent') + ->willReturn(true); + + $this->keyManagerMock->expects($this->exactly(4)) + ->method('setPrivateKey'); + + $this->assertNull($this->instance->setPassphrase($this->params)); + $this->params['recoveryPassword'] = 'password'; + + $this->recoveryMock->expects($this->exactly(3)) + ->method('isRecoveryEnabledForUser') + ->with('testUser') + ->willReturnOnConsecutiveCalls(true, false); + + + // Test first if statement + $this->assertNull($this->instance->setPassphrase($this->params)); + + // Test Second if conditional + $this->keyManagerMock->expects($this->exactly(2)) + ->method('userHasKeys') + ->with('testUser') + ->willReturn(true); + + $this->assertNull($this->instance->setPassphrase($this->params)); + + // Test third and final if condition + $this->utilMock->expects($this->once()) + ->method('userHasFiles') + ->with('testUser') + ->willReturn(false); + + $this->cryptMock->expects($this->once()) + ->method('createKeyPair'); + + $this->keyManagerMock->expects($this->once()) + ->method('setPrivateKey'); + + $this->recoveryMock->expects($this->once()) + ->method('recoverUsersFiles') + ->with('password', 'testUser'); + + $this->assertNull($this->instance->setPassphrase($this->params)); + } + + public function testPostPasswordReset() { + $this->keyManagerMock->expects($this->once()) + ->method('replaceUserKeys') + ->with('testUser'); + + $this->userSetupMock->expects($this->once()) + ->method('setupServerSide') + ->with('testUser', 'password'); + + $this->assertNull($this->instance->postPasswordReset($this->params)); + } + + protected function setUp() { + parent::setUp(); + $loggerMock = $this->getMock('OCP\ILogger'); + $this->keyManagerMock = $this->getMockBuilder('OCA\Encryption\KeyManager') + ->disableOriginalConstructor() + ->getMock(); + $this->userSetupMock = $this->getMockBuilder('OCA\Encryption\Users\Setup') + ->disableOriginalConstructor() + ->getMock(); + + $this->userSessionMock = $this->getMockBuilder('OCP\IUserSession') + ->disableOriginalConstructor() + ->setMethods([ + 'isLoggedIn', + 'getUID', + 'login', + 'logout', + 'setUser', + 'getUser', + 'canChangePassword' + ]) + ->getMock(); + + $this->userSessionMock->expects($this->any())->method('getUID')->will($this->returnValue('testUser')); + + $this->userSessionMock->expects($this->any()) + ->method($this->anything()) + ->will($this->returnSelf()); + + $utilMock = $this->getMockBuilder('OCA\Encryption\Util') + ->disableOriginalConstructor() + ->getMock(); + + $sessionMock = $this->getMockBuilder('OCA\Encryption\Session') + ->disableOriginalConstructor() + ->getMock(); + + $this->cryptMock = $this->getMockBuilder('OCA\Encryption\Crypto\Crypt') + ->disableOriginalConstructor() + ->getMock(); + $recoveryMock = $this->getMockBuilder('OCA\Encryption\Recovery') + ->disableOriginalConstructor() + ->getMock(); + + $this->sessionMock = $sessionMock; + $this->recoveryMock = $recoveryMock; + $this->utilMock = $utilMock; + $this->instance = new UserHooks($this->keyManagerMock, + $loggerMock, + $this->userSetupMock, + $this->userSessionMock, + $this->utilMock, + $this->sessionMock, + $this->cryptMock, + $this->recoveryMock + ); + + } + +} diff --git a/apps/encryption/tests/lib/HookManagerTest.php b/apps/encryption/tests/lib/HookManagerTest.php index 3c360ff3504..3da0bafb691 100644 --- a/apps/encryption/tests/lib/HookManagerTest.php +++ b/apps/encryption/tests/lib/HookManagerTest.php @@ -1,7 +1,7 @@ <?php /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 3/31/15, 1:54 PM + * @author Clark Tomlinson <fallen013@gmail.com> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * diff --git a/apps/encryption/tests/lib/KeyManagerTest.php b/apps/encryption/tests/lib/KeyManagerTest.php index d12578bb8d2..1e51341a7e4 100644 --- a/apps/encryption/tests/lib/KeyManagerTest.php +++ b/apps/encryption/tests/lib/KeyManagerTest.php @@ -1,9 +1,23 @@ <?php /** - * @author Clark Tomlinson <fallen013@gmail.com> - * @since 3/5/15, 10:53 AM - * @link http:/www.clarkt.com - * @copyright Clark Tomlinson © 2015 + * @author Björn Schießle <schiessle@owncloud.com> + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> * */ diff --git a/apps/encryption/tests/lib/RecoveryTest.php b/apps/encryption/tests/lib/RecoveryTest.php index 701762b56d6..b3fd403949c 100644 --- a/apps/encryption/tests/lib/RecoveryTest.php +++ b/apps/encryption/tests/lib/RecoveryTest.php @@ -1,7 +1,7 @@ <?php /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 4/3/15, 9:57 AM + * @author Clark Tomlinson <fallen013@gmail.com> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * diff --git a/apps/encryption/tests/lib/SessionTest.php b/apps/encryption/tests/lib/SessionTest.php index f7e026808f0..e036c439939 100644 --- a/apps/encryption/tests/lib/SessionTest.php +++ b/apps/encryption/tests/lib/SessionTest.php @@ -1,7 +1,8 @@ <?php /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 3/31/15, 10:19 AM + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * diff --git a/apps/encryption/tests/lib/UtilTest.php b/apps/encryption/tests/lib/UtilTest.php index fa87a629f2f..5f086a8e475 100644 --- a/apps/encryption/tests/lib/UtilTest.php +++ b/apps/encryption/tests/lib/UtilTest.php @@ -1,7 +1,7 @@ <?php /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 3/31/15, 3:49 PM + * @author Clark Tomlinson <fallen013@gmail.com> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * diff --git a/apps/encryption/tests/lib/crypto/encryptionTest.php b/apps/encryption/tests/lib/crypto/encryptionTest.php index 52a322463a9..9e14a70ebb0 100644 --- a/apps/encryption/tests/lib/crypto/encryptionTest.php +++ b/apps/encryption/tests/lib/crypto/encryptionTest.php @@ -1,24 +1,22 @@ <?php - /** - * ownCloud - * - * @copyright (C) 2015 ownCloud, Inc. + * @author Björn Schießle <schiessle@owncloud.com> * - * @author Bjoern Schiessle <schiessle@owncloud.com> + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. + * 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 library is distributed in the hope that it will be useful, + * 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. + * 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/> * - * You should have received a copy of the GNU Affero General Public - * License along with this library. If not, see <http://www.gnu.org/licenses/>. */ namespace OCA\Encryption\Tests\Crypto; diff --git a/apps/encryption/tests/lib/users/SetupTest.php b/apps/encryption/tests/lib/users/SetupTest.php index 6a66df3674b..354de26253e 100644 --- a/apps/encryption/tests/lib/users/SetupTest.php +++ b/apps/encryption/tests/lib/users/SetupTest.php @@ -1,7 +1,7 @@ <?php /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 4/6/15, 11:50 AM + * @author Clark Tomlinson <fallen013@gmail.com> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * diff --git a/apps/encryption_dummy/lib/dummymodule.php b/apps/encryption_dummy/lib/dummymodule.php index 8cec9dfaf4c..b4dfe34a9bf 100644 --- a/apps/encryption_dummy/lib/dummymodule.php +++ b/apps/encryption_dummy/lib/dummymodule.php @@ -56,11 +56,11 @@ class DummyModule implements IEncryptionModule { * @param array $header contains the header data read from the file * @param array $accessList who has access to the file contains the key 'users' and 'public' * - * $return array $header contain data as key-value pairs which should be + * @return array $header contain data as key-value pairs which should be * written to the header, in case of a write operation * or if no additional data is needed return a empty array */ - public function begin($path, $user, $header, $accessList) { + public function begin($path, $user, array $header, array $accessList) { return array(); } @@ -141,7 +141,7 @@ class DummyModule implements IEncryptionModule { * @param array $accessList who has access to the file contains the key 'users' and 'public' * @return boolean */ - public function update($path, $uid, $accessList) { + public function update($path, $uid, array $accessList) { return true; } } diff --git a/apps/files/appinfo/app.php b/apps/files/appinfo/app.php index e4cac7c7c14..c483ad31ec5 100644 --- a/apps/files/appinfo/app.php +++ b/apps/files/appinfo/app.php @@ -59,6 +59,11 @@ $templateManager->registerTemplate('application/vnd.oasis.opendocument.spreadshe \OC::$server->getActivityManager()->registerExtension(function() { return new \OCA\Files\Activity( \OC::$server->query('L10NFactory'), - \OC::$server->getURLGenerator() + \OC::$server->getURLGenerator(), + \OC::$server->getActivityManager(), + new \OCA\Files\ActivityHelper( + \OC::$server->getTagManager() + ), + \OC::$server->getConfig() ); }); diff --git a/apps/files/css/files.css b/apps/files/css/files.css index d34013db499..455ccae3f96 100644 --- a/apps/files/css/files.css +++ b/apps/files/css/files.css @@ -103,6 +103,27 @@ min-height: 100%; } +/* icons for sidebar */ +.nav-icon-files { + background-image: url('../img/folder.svg'); +} +.nav-icon-favorites { + background-image: url('../img/star.svg'); +} +.nav-icon-sharingin, +.nav-icon-sharingout { + background-image: url('../img/share.svg'); +} +.nav-icon-sharinglinks { + background-image: url('../img/public.svg'); +} +.nav-icon-extstoragemounts { + background-image: url('../img/external.svg'); +} +.nav-icon-trashbin { + background-image: url('../img/delete.svg'); +} + /* move Deleted Files to bottom of sidebar */ .nav-trashbin { position: fixed !important; diff --git a/apps/files/img/delete.png b/apps/files/img/delete.png Binary files differnew file mode 100644 index 00000000000..b2ab8c5efef --- /dev/null +++ b/apps/files/img/delete.png diff --git a/apps/files/img/delete.svg b/apps/files/img/delete.svg new file mode 100644 index 00000000000..b6dc2cc4173 --- /dev/null +++ b/apps/files/img/delete.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> + <path opacity=".5" d="m6.5 1-0.5 1h-3c-0.554 0-1 0.446-1 1v1h12v-1c0-0.554-0.446-1-1-1h-3l-0.5-1zm-3.5 4 0.875 9c0.061 0.549 0.5729 1 1.125 1h6c0.55232 0 1.064-0.45102 1.125-1l0.875-9z" fill-rule="evenodd"/> +</svg> diff --git a/apps/files/img/external.png b/apps/files/img/external.png Binary files differnew file mode 100644 index 00000000000..2ac5e9344c3 --- /dev/null +++ b/apps/files/img/external.png diff --git a/apps/files/img/external.svg b/apps/files/img/external.svg new file mode 100644 index 00000000000..d1940f2f1b3 --- /dev/null +++ b/apps/files/img/external.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> + <path opacity=".5" d="m7.4515 1.6186 2.3806 2.2573-3.5709 3.386 2.3806 2.2573 3.5709-3.386 2.3806 2.2573v-6.7725h-7.1422zm-4.7612 1.1286c-0.65945 0-1.1903 0.5034-1.1903 1.1286v9.029c0 0.6253 0.53085 1.1286 1.1903 1.1286h9.522c0.6594 0 1.1903-0.5034 1.1903-1.1286v-3.386l-1.19-1.1287v4.5146h-9.5217v-9.029h4.761l-1.1905-1.1287h-3.5707z"/> +</svg> diff --git a/apps/files/img/folder.png b/apps/files/img/folder.png Binary files differnew file mode 100644 index 00000000000..ada4c4c2f88 --- /dev/null +++ b/apps/files/img/folder.png diff --git a/apps/files/img/folder.svg b/apps/files/img/folder.svg new file mode 100644 index 00000000000..1a97808443d --- /dev/null +++ b/apps/files/img/folder.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> + <g opacity=".5" fill-rule="evenodd" transform="matrix(.86667 0 0 .86667 -172.04 -864.43)"> + <path d="m200.2 998.57c-0.28913 0-0.53125 0.24212-0.53125 0.53125v13.938c0 0.2985 0.23264 0.5312 0.53125 0.5312h15.091c0.2986 0 0.53125-0.2327 0.53125-0.5312l0.0004-8.1661c0-0.289-0.24212-0.5338-0.53125-0.5338h-12.161l-0.0004 6.349c0 0.2771-0.30237 0.5638-0.57937 0.5638s-0.57447-0.2867-0.57447-0.5638l0.0004-7.0055c0-0.2771 0.23357-0.4974 0.51057-0.4974h2.6507l8.3774 0.0003-0.0004-1.7029c0-0.3272-0.24549-0.6047-0.57258-0.6047h-7.5043v-1.7764c0-0.28915-0.23415-0.53125-0.52328-0.53125z" fill-rule="evenodd"/> + </g> +</svg> diff --git a/apps/files/img/public.png b/apps/files/img/public.png Binary files differnew file mode 100644 index 00000000000..f49c5fb1ee5 --- /dev/null +++ b/apps/files/img/public.png diff --git a/apps/files/img/public.svg b/apps/files/img/public.svg new file mode 100644 index 00000000000..23ac51d8367 --- /dev/null +++ b/apps/files/img/public.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> + <g opacity=".5" transform="matrix(.67042 -.67042 .67042 .67042 .62542 93.143)"> + <path style="block-progression:tb;color:#000000;text-transform:none;text-indent:0" d="m69.5-61.5c-1.9217 0-3.5 1.5783-3.5 3.5 0 0.17425 0.0062 0.33232 0.03125 0.5h2.0625c-0.053-0.156-0.094-0.323-0.094-0.5 0-0.8483 0.6517-1.5 1.5-1.5h5c0.8483 0 1.5 0.6517 1.5 1.5s-0.6517 1.5-1.5 1.5h-1.6875c-0.28733 0.79501-0.78612 1.4793-1.4375 2h3.125c1.9217 0 3.5-1.5783 3.5-3.5s-1.5783-3.5-3.5-3.5h-5z"/> + <path style="block-progression:tb;color:#000000;text-transform:none;text-indent:0" d="m68.5-54.5c1.9217 0 3.5-1.5783 3.5-3.5 0-0.17425-0.0062-0.33232-0.03125-0.5h-2.0625c0.053 0.156 0.094 0.323 0.094 0.5 0 0.8483-0.6517 1.5-1.5 1.5h-5c-0.8483 0-1.5-0.6517-1.5-1.5s0.6517-1.5 1.5-1.5h1.6875c0.28733-0.79501 0.78612-1.4793 1.4375-2h-3.125c-1.9217 0-3.5 1.5783-3.5 3.5s1.5783 3.5 3.5 3.5h5z"/> + </g> +</svg> diff --git a/apps/files/img/share.png b/apps/files/img/share.png Binary files differnew file mode 100644 index 00000000000..61c87e78b6c --- /dev/null +++ b/apps/files/img/share.png diff --git a/apps/files/img/share.svg b/apps/files/img/share.svg new file mode 100644 index 00000000000..97f52f2e783 --- /dev/null +++ b/apps/files/img/share.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> + <g opacity=".5" transform="translate(0 -1036.4)"> + <path d="m12.228 1037.4c-1.3565 0-2.4592 1.0977-2.4592 2.4542 0 0.075 0.0084 0.1504 0.0149 0.2236l-4.7346 2.4145c-0.4291-0.3667-0.98611-0.5863-1.5947-0.5863-1.3565 0-2.4542 1.0977-2.4542 2.4543 0 1.3565 1.0977 2.4542 2.4542 2.4542 0.54607 0 1.0528-0.1755 1.4606-0.477l4.8637 2.4741c-0.0024 0.044-0.0099 0.089-0.0099 0.1342 0 1.3565 1.1027 2.4542 2.4592 2.4542s2.4542-1.0977 2.4542-2.4542-1.0977-2.4592-2.4542-2.4592c-0.63653 0-1.218 0.2437-1.6544 0.6409l-4.6953-2.4c0.01892-0.1228 0.03478-0.2494 0.03478-0.3775 0-0.072-0.0089-0.1437-0.0149-0.2137l4.7395-2.4145c0.42802 0.3627 0.98488 0.5813 1.5898 0.5813 1.3565 0 2.4542-1.1027 2.4542-2.4592s-1.0977-2.4542-2.4542-2.4542z"/> + </g> +</svg> diff --git a/apps/files/img/star.png b/apps/files/img/star.png Binary files differnew file mode 100644 index 00000000000..6b0ec67ec1c --- /dev/null +++ b/apps/files/img/star.png diff --git a/apps/files/img/star.svg b/apps/files/img/star.svg new file mode 100644 index 00000000000..fb3eef1e452 --- /dev/null +++ b/apps/files/img/star.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> + <g opacity=".5" transform="matrix(.049689 0 0 .049689 -7.3558 -36.792)"> + <path d="m330.36 858.43 43.111 108.06 117.64 9.2572-89.445 74.392 27.55 114.75-98.391-62.079-100.62 61.66 28.637-112.76-89.734-76.638 116.09-7.6094z" transform="translate(-21.071,-112.5)"/> + </g> +</svg> diff --git a/apps/files/lib/activity.php b/apps/files/lib/activity.php index c376dc7b147..fff49ea4ea5 100644 --- a/apps/files/lib/activity.php +++ b/apps/files/lib/activity.php @@ -24,16 +24,20 @@ namespace OCA\Files; use OC\L10N\Factory; use OCP\Activity\IExtension; +use OCP\Activity\IManager; +use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; class Activity implements IExtension { const FILTER_FILES = 'files'; + const FILTER_FAVORITES = 'files_favorites'; const TYPE_SHARE_CREATED = 'file_created'; const TYPE_SHARE_CHANGED = 'file_changed'; const TYPE_SHARE_DELETED = 'file_deleted'; const TYPE_SHARE_RESTORED = 'file_restored'; + const TYPE_FAVORITES = 'files_favorites'; /** @var IL10N */ protected $l; @@ -44,14 +48,29 @@ class Activity implements IExtension { /** @var IURLGenerator */ protected $URLGenerator; + /** @var \OCP\Activity\IManager */ + protected $activityManager; + + /** @var \OCP\IConfig */ + protected $config; + + /** @var \OCA\Files\ActivityHelper */ + protected $helper; + /** * @param Factory $languageFactory * @param IURLGenerator $URLGenerator + * @param IManager $activityManager + * @param ActivityHelper $helper + * @param IConfig $config */ - public function __construct(Factory $languageFactory, IURLGenerator $URLGenerator) { + public function __construct(Factory $languageFactory, IURLGenerator $URLGenerator, IManager $activityManager, ActivityHelper $helper, IConfig $config) { $this->languageFactory = $languageFactory; $this->URLGenerator = $URLGenerator; $this->l = $this->getL10N(); + $this->activityManager = $activityManager; + $this->helper = $helper; + $this->config = $config; } /** @@ -74,6 +93,7 @@ class Activity implements IExtension { return [ self::TYPE_SHARE_CREATED => (string) $l->t('A new file or folder has been <strong>created</strong>'), self::TYPE_SHARE_CHANGED => (string) $l->t('A file or folder has been <strong>changed</strong>'), + self::TYPE_FAVORITES => (string) $l->t('Limit notifications about creation and changes to your <strong>favorite files</strong> <em>(Stream only)</em>'), self::TYPE_SHARE_DELETED => (string) $l->t('A file or folder has been <strong>deleted</strong>'), self::TYPE_SHARE_RESTORED => (string) $l->t('A file or folder has been <strong>restored</strong>'), ]; @@ -229,6 +249,13 @@ class Activity implements IExtension { */ public function getNavigation() { return [ + 'top' => [ + self::FILTER_FAVORITES => [ + 'id' => self::FILTER_FAVORITES, + 'name' => (string) $this->l->t('Favorites'), + 'url' => $this->URLGenerator->linkToRoute('activity.Activities.showList', ['filter' => self::FILTER_FAVORITES]), + ], + ], 'apps' => [ self::FILTER_FILES => [ 'id' => self::FILTER_FILES, @@ -236,7 +263,6 @@ class Activity implements IExtension { 'url' => $this->URLGenerator->linkToRoute('activity.Activities.showList', ['filter' => self::FILTER_FILES]), ], ], - 'top' => [], ]; } @@ -247,7 +273,7 @@ class Activity implements IExtension { * @return boolean */ public function isFilterValid($filterValue) { - return $filterValue === self::FILTER_FILES; + return $filterValue === self::FILTER_FILES || $filterValue === self::FILTER_FAVORITES; } /** @@ -259,7 +285,7 @@ class Activity implements IExtension { * @return array|false */ public function filterNotificationTypes($types, $filter) { - if ($filter === self::FILTER_FILES) { + if ($filter === self::FILTER_FILES || $filter === self::FILTER_FAVORITES) { return array_intersect([ self::TYPE_SHARE_CREATED, self::TYPE_SHARE_CHANGED, @@ -280,9 +306,63 @@ class Activity implements IExtension { * @return array|false */ public function getQueryForFilter($filter) { + $user = $this->activityManager->getCurrentUserId(); + // Display actions from all files if ($filter === self::FILTER_FILES) { return ['`app` = ?', ['files']]; } + + if (!$user) { + // Remaining filters only work with a user/token + return false; + } + + // Display actions from favorites only + if ($filter === self::FILTER_FAVORITES || in_array($filter, ['all', 'by', 'self']) && $this->userSettingFavoritesOnly($user)) { + try { + $favorites = $this->helper->getFavoriteFilePaths($user); + } catch (\RuntimeException $e) { + // Too many favorites, can not put them into one query anymore... + return ['`app` = ?', ['files']]; + } + + /* + * Display activities only, when they are not `type` create/change + * or `file` is a favorite or in a favorite folder + */ + $parameters = $fileQueryList = []; + $parameters[] = 'files'; + + $fileQueryList[] = '(`type` <> ? AND `type` <> ?)'; + $parameters[] = self::TYPE_SHARE_CREATED; + $parameters[] = self::TYPE_SHARE_CHANGED; + + foreach ($favorites['items'] as $favorite) { + $fileQueryList[] = '`file` = ?'; + $parameters[] = $favorite; + } + foreach ($favorites['folders'] as $favorite) { + $fileQueryList[] = '`file` LIKE ?'; + $parameters[] = $favorite . '/%'; + } + + $parameters[] = 'files'; + + return [ + ' CASE WHEN `app` = ? THEN (' . implode(' OR ', $fileQueryList) . ') ELSE `app` <> ? END ', + $parameters, + ]; + } return false; } + + /** + * Is the file actions favorite limitation enabled? + * + * @param string $user + * @return bool + */ + protected function userSettingFavoritesOnly($user) { + return (bool) $this->config->getUserValue($user, 'activity', 'notify_stream_' . self::TYPE_FAVORITES, false); + } } diff --git a/apps/files/lib/activityhelper.php b/apps/files/lib/activityhelper.php new file mode 100644 index 00000000000..f9ff722b1c2 --- /dev/null +++ b/apps/files/lib/activityhelper.php @@ -0,0 +1,84 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Files; + +use OCP\Files\Folder; +use OCP\ITagManager; + +class ActivityHelper { + /** If a user has a lot of favorites the query might get too slow and long */ + const FAVORITE_LIMIT = 50; + + /** @var \OCP\ITagManager */ + protected $tagManager; + + /** + * @param ITagManager $tagManager + */ + public function __construct(ITagManager $tagManager) { + $this->tagManager = $tagManager; + } + + /** + * Returns an array with the favorites + * + * @param string $user + * @return array + * @throws \RuntimeException when too many or no favorites where found + */ + public function getFavoriteFilePaths($user) { + $tags = $this->tagManager->load('files', [], false, $user); + $favorites = $tags->getFavorites(); + + if (empty($favorites)) { + throw new \RuntimeException('No favorites', 1); + } else if (isset($favorites[self::FAVORITE_LIMIT])) { + throw new \RuntimeException('Too many favorites', 2); + } + + // Can not DI because the user is not known on instantiation + $rootFolder = \OC::$server->getUserFolder($user); + $folders = $items = []; + foreach ($favorites as $favorite) { + $nodes = $rootFolder->getById($favorite); + if (!empty($nodes)) { + /** @var \OCP\Files\Node $node */ + $node = array_shift($nodes); + $path = substr($node->getPath(), strlen($user . '/files/')); + + $items[] = $path; + if ($node instanceof Folder) { + $folders[] = $path; + } + } + } + + if (empty($items)) { + throw new \RuntimeException('No favorites', 1); + } + + return [ + 'items' => $items, + 'folders' => $folders, + ]; + } +} diff --git a/apps/files/templates/appnavigation.php b/apps/files/templates/appnavigation.php index e6237c7f485..f586b0ecb28 100644 --- a/apps/files/templates/appnavigation.php +++ b/apps/files/templates/appnavigation.php @@ -1,13 +1,18 @@ <div id="app-navigation"> - <ul> + <ul class="with-icon"> <?php foreach ($_['navigationItems'] as $item) { ?> - <li data-id="<?php p($item['id']) ?>" class="nav-<?php p($item['id']) ?>"><a href="<?php p(isset($item['href']) ? $item['href'] : '#') ?>"><?php p($item['name']);?></a></li> + <li data-id="<?php p($item['id']) ?>" class="nav-<?php p($item['id']) ?>"> + <a href="<?php p(isset($item['href']) ? $item['href'] : '#') ?>" + class="nav-icon-<?php p($item['id']) ?> svg"> + <?php p($item['name']);?> + </a> + </li> <?php } ?> </ul> <div id="app-settings"> <div id="app-settings-header"> <button class="settings-button" data-apps-slide-toggle="#app-settings-content"> - <span class="hidden-visually"><?php p($l->t('Settings'));?></span> + <span><?php p($l->t('Settings'));?></span> </button> </div> <div id="app-settings-content"> diff --git a/apps/files/tests/activitytest.php b/apps/files/tests/activitytest.php new file mode 100644 index 00000000000..1f8d6330e51 --- /dev/null +++ b/apps/files/tests/activitytest.php @@ -0,0 +1,297 @@ +<?php +/** + * Copyright (c) 2015 Joas Schilling <nickvergessen@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + * +*/ + +namespace OCA\Files\Tests; + +use OCA\Files\Activity; +use Test\TestCase; + +class ActivityTest extends TestCase { + + /** @var \OC\ActivityManager */ + private $activityManager; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $request; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $session; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $config; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $activityHelper; + + /** @var \OCA\Files\Activity */ + protected $activityExtension; + + protected function setUp() { + parent::setUp(); + + $this->request = $this->getMockBuilder('OCP\IRequest') + ->disableOriginalConstructor() + ->getMock(); + $this->session = $this->getMockBuilder('OCP\IUserSession') + ->disableOriginalConstructor() + ->getMock(); + $this->config = $this->getMockBuilder('OCP\IConfig') + ->disableOriginalConstructor() + ->getMock(); + $this->activityHelper = $this->getMockBuilder('OCA\Files\ActivityHelper') + ->disableOriginalConstructor() + ->getMock(); + + $this->activityManager = new \OC\ActivityManager( + $this->request, + $this->session, + $this->config + ); + + $this->activityExtension = $activityExtension = new Activity( + new \OC\L10N\Factory(), + $this->getMockBuilder('OCP\IURLGenerator')->disableOriginalConstructor()->getMock(), + $this->activityManager, + $this->activityHelper, + $this->config + ); + + $this->activityManager->registerExtension(function() use ($activityExtension) { + return $activityExtension; + }); + } + + public function testNotificationTypes() { + $result = $this->activityExtension->getNotificationTypes('en'); + $this->assertTrue(is_array($result), 'Asserting getNotificationTypes() returns an array'); + $this->assertCount(5, $result); + $this->assertArrayHasKey(Activity::TYPE_SHARE_CREATED, $result); + $this->assertArrayHasKey(Activity::TYPE_SHARE_CHANGED, $result); + $this->assertArrayHasKey(Activity::TYPE_FAVORITES, $result); + $this->assertArrayHasKey(Activity::TYPE_SHARE_DELETED, $result); + $this->assertArrayHasKey(Activity::TYPE_SHARE_RESTORED, $result); + } + + public function testDefaultTypes() { + $result = $this->activityExtension->getDefaultTypes('stream'); + $this->assertTrue(is_array($result), 'Asserting getDefaultTypes(stream) returns an array'); + $this->assertCount(4, $result); + $result = array_flip($result); + $this->assertArrayHasKey(Activity::TYPE_SHARE_CREATED, $result); + $this->assertArrayHasKey(Activity::TYPE_SHARE_CHANGED, $result); + $this->assertArrayNotHasKey(Activity::TYPE_FAVORITES, $result); + $this->assertArrayHasKey(Activity::TYPE_SHARE_DELETED, $result); + $this->assertArrayHasKey(Activity::TYPE_SHARE_RESTORED, $result); + + $result = $this->activityExtension->getDefaultTypes('email'); + $this->assertFalse($result, 'Asserting getDefaultTypes(email) returns false'); + } + + public function testTranslate() { + $this->assertFalse( + $this->activityExtension->translate('files_sharing', '', [], false, false, 'en'), + 'Asserting that no translations are set for files_sharing' + ); + } + + public function testGetSpecialParameterList() { + $this->assertFalse( + $this->activityExtension->getSpecialParameterList('files_sharing', ''), + 'Asserting that no special parameters are set for files_sharing' + ); + } + + public function typeIconData() { + return [ + [Activity::TYPE_SHARE_CHANGED, 'icon-change'], + [Activity::TYPE_SHARE_CREATED, 'icon-add-color'], + [Activity::TYPE_SHARE_DELETED, 'icon-delete-color'], + [Activity::TYPE_SHARE_RESTORED, false], + [Activity::TYPE_FAVORITES, false], + ['unknown type', false], + ]; + } + + /** + * @dataProvider typeIconData + * + * @param string $type + * @param mixed $expected + */ + public function testTypeIcon($type, $expected) { + $this->assertSame($expected, $this->activityExtension->getTypeIcon($type)); + } + + public function testGroupParameter() { + $this->assertFalse( + $this->activityExtension->getGroupParameter(['app' => 'files_sharing']), + 'Asserting that no group parameters are set for files_sharing' + ); + } + + public function testNavigation() { + $result = $this->activityExtension->getNavigation(); + $this->assertCount(1, $result['top']); + $this->assertArrayHasKey(Activity::FILTER_FAVORITES, $result['top']); + + $this->assertCount(1, $result['apps']); + $this->assertArrayHasKey(Activity::FILTER_FILES, $result['apps']); + } + + public function testIsFilterValid() { + $this->assertTrue($this->activityExtension->isFilterValid(Activity::FILTER_FAVORITES)); + $this->assertTrue($this->activityExtension->isFilterValid(Activity::FILTER_FILES)); + $this->assertFalse($this->activityExtension->isFilterValid('unknown filter')); + } + + public function filterNotificationTypesData() { + return [ + [ + Activity::FILTER_FILES, + [ + 'NT0', + Activity::TYPE_SHARE_CREATED, + Activity::TYPE_SHARE_CHANGED, + Activity::TYPE_SHARE_DELETED, + Activity::TYPE_SHARE_RESTORED, + Activity::TYPE_FAVORITES, + ], [ + Activity::TYPE_SHARE_CREATED, + Activity::TYPE_SHARE_CHANGED, + Activity::TYPE_SHARE_DELETED, + Activity::TYPE_SHARE_RESTORED, + ], + ], + [ + Activity::FILTER_FILES, + [ + 'NT0', + Activity::TYPE_SHARE_CREATED, + Activity::TYPE_FAVORITES, + ], + [ + Activity::TYPE_SHARE_CREATED, + ], + ], + [ + Activity::FILTER_FAVORITES, + [ + 'NT0', + Activity::TYPE_SHARE_CREATED, + Activity::TYPE_SHARE_CHANGED, + Activity::TYPE_SHARE_DELETED, + Activity::TYPE_SHARE_RESTORED, + Activity::TYPE_FAVORITES, + ], [ + Activity::TYPE_SHARE_CREATED, + Activity::TYPE_SHARE_CHANGED, + Activity::TYPE_SHARE_DELETED, + Activity::TYPE_SHARE_RESTORED, + ], + ], + [ + 'unknown filter', + [ + 'NT0', + Activity::TYPE_SHARE_CREATED, + Activity::TYPE_SHARE_CHANGED, + Activity::TYPE_SHARE_DELETED, + Activity::TYPE_SHARE_RESTORED, + Activity::TYPE_FAVORITES, + ], + false, + ], + ]; + } + + /** + * @dataProvider filterNotificationTypesData + * + * @param string $filter + * @param array $types + * @param mixed $expected + */ + public function testFilterNotificationTypes($filter, $types, $expected) { + $result = $this->activityExtension->filterNotificationTypes($types, $filter); + $this->assertEquals($expected, $result); + } + + public function queryForFilterData() { + return [ + [ + new \RuntimeException(), + '`app` = ?', + ['files'] + ], + [ + [ + 'items' => [], + 'folders' => [], + ], + ' CASE WHEN `app` = ? THEN ((`type` <> ? AND `type` <> ?)) ELSE `app` <> ? END ', + ['files', Activity::TYPE_SHARE_CREATED, Activity::TYPE_SHARE_CHANGED, 'files'] + ], + [ + [ + 'items' => ['file.txt', 'folder'], + 'folders' => ['folder'], + ], + ' CASE WHEN `app` = ? THEN ((`type` <> ? AND `type` <> ?) OR `file` = ? OR `file` = ? OR `file` LIKE ?) ELSE `app` <> ? END ', + ['files', Activity::TYPE_SHARE_CREATED, Activity::TYPE_SHARE_CHANGED, 'file.txt', 'folder', 'folder/%', 'files'] + ], + ]; + } + + /** + * @dataProvider queryForFilterData + * + * @param mixed $will + * @param string $query + * @param array $parameters + */ + public function testQueryForFilter($will, $query, $parameters) { + $this->mockUserSession('test'); + + $this->config->expects($this->any()) + ->method('getUserValue') + ->willReturnMap([ + ['test', 'activity', 'notify_stream_' . Activity::TYPE_FAVORITES, false, true], + ]); + if (is_array($will)) { + $this->activityHelper->expects($this->any()) + ->method('getFavoriteFilePaths') + ->with('test') + ->willReturn($will); + } else { + $this->activityHelper->expects($this->any()) + ->method('getFavoriteFilePaths') + ->with('test') + ->willThrowException($will); + } + + $result = $this->activityExtension->getQueryForFilter('all'); + $this->assertEquals([$query, $parameters], $result); + } + + protected function mockUserSession($user) { + $mockUser = $this->getMockBuilder('\OCP\IUser') + ->disableOriginalConstructor() + ->getMock(); + $mockUser->expects($this->any()) + ->method('getUID') + ->willReturn($user); + + $this->session->expects($this->any()) + ->method('isLoggedIn') + ->willReturn(true); + $this->session->expects($this->any()) + ->method('getUser') + ->willReturn($mockUser); + } +} diff --git a/apps/files/tests/ajax_rename.php b/apps/files/tests/ajax_rename.php index 789177bb353..34e7f5085dd 100644 --- a/apps/files/tests/ajax_rename.php +++ b/apps/files/tests/ajax_rename.php @@ -38,21 +38,15 @@ class Test_OC_Files_App_Rename extends \Test\TestCase { */ private $files; - private $originalStorage; - protected function setUp() { parent::setUp(); - $this->originalStorage = \OC\Files\Filesystem::getStorage('/'); - // mock OC_L10n if (!self::$user) { self::$user = uniqid(); } \OC_User::createUser(self::$user, 'password'); - \OC_User::setUserId(self::$user); - - \OC\Files\Filesystem::init(self::$user, '/' . self::$user . '/files'); + $this->loginAsUser(self::$user); $l10nMock = $this->getMock('\OC_L10N', array('t'), array(), '', false); $l10nMock->expects($this->any()) @@ -72,9 +66,8 @@ class Test_OC_Files_App_Rename extends \Test\TestCase { protected function tearDown() { $result = \OC_User::deleteUser(self::$user); $this->assertTrue($result); - \OC\Files\Filesystem::tearDown(); - \OC\Files\Filesystem::mount($this->originalStorage, array(), '/'); + $this->logout(); parent::tearDown(); } diff --git a/apps/files_external/lib/sftp_key.php b/apps/files_external/lib/sftp_key.php index c460d81b8f0..d9bcadb9eb7 100644 --- a/apps/files_external/lib/sftp_key.php +++ b/apps/files_external/lib/sftp_key.php @@ -135,11 +135,16 @@ class SFTP_Key extends \OC\Files\Storage\SFTP { } public function test() { - if (empty($this->getHost())) { + + // FIXME: Use as expression in empty once PHP 5.4 support is dropped + $host = $this->getHost(); + if (empty($host)) { \OC::$server->getLogger()->warning('Hostname has not been specified'); return false; } - if (empty($this->getUser())) { + // FIXME: Use as expression in empty once PHP 5.4 support is dropped + $user = $this->getUser(); + if (empty($user)) { \OC::$server->getLogger()->warning('Username has not been specified'); return false; } diff --git a/apps/files_external/service/globalstoragesservice.php b/apps/files_external/service/globalstoragesservice.php index 011730390b0..7df0f73f652 100644 --- a/apps/files_external/service/globalstoragesservice.php +++ b/apps/files_external/service/globalstoragesservice.php @@ -101,6 +101,7 @@ class GlobalStoragesService extends StoragesService { * @param string $signal signal to trigger */ protected function triggerHooks(StorageConfig $storage, $signal) { + // FIXME: Use as expression in empty once PHP 5.4 support is dropped $applicableUsers = $storage->getApplicableUsers(); $applicableGroups = $storage->getApplicableGroups(); if (empty($applicableUsers) && empty($applicableGroups)) { @@ -149,8 +150,11 @@ class GlobalStoragesService extends StoragesService { $groupAdditions = array_diff($newStorage->getApplicableGroups(), $oldStorage->getApplicableGroups()); $groupDeletions = array_diff($oldStorage->getApplicableGroups(), $newStorage->getApplicableGroups()); + // FIXME: Use as expression in empty once PHP 5.4 support is dropped // if no applicable were set, raise a signal for "all" - if (empty($oldStorage->getApplicableUsers()) && empty($oldStorage->getApplicableGroups())) { + $oldApplicableUsers = $oldStorage->getApplicableUsers(); + $oldApplicableGroups = $oldStorage->getApplicableGroups(); + if (empty($oldApplicableUsers) && empty($oldApplicableGroups)) { $this->triggerApplicableHooks( Filesystem::signal_delete_mount, $oldStorage->getMountPoint(), @@ -191,8 +195,11 @@ class GlobalStoragesService extends StoragesService { $groupAdditions ); + // FIXME: Use as expression in empty once PHP 5.4 support is dropped // if no applicable, raise a signal for "all" - if (empty($newStorage->getApplicableUsers()) && empty($newStorage->getApplicableGroups())) { + $newApplicableUsers = $newStorage->getApplicableUsers(); + $newApplicableGroups = $newStorage->getApplicableGroups(); + if (empty($newApplicableUsers) && empty($newApplicableGroups)) { $this->triggerApplicableHooks( Filesystem::signal_create_mount, $newStorage->getMountPoint(), diff --git a/apps/files_external/service/storagesservice.php b/apps/files_external/service/storagesservice.php index 399a56677bf..51eb4abcc00 100644 --- a/apps/files_external/service/storagesservice.php +++ b/apps/files_external/service/storagesservice.php @@ -227,8 +227,10 @@ abstract class StoragesService { if (!is_null($storageConfig->getPriority())) { $options['priority'] = $storageConfig->getPriority(); } - if (!empty($storageConfig->getMountOptions())) { - $options['mountOptions'] = $storageConfig->getMountOptions(); + + $mountOptions = $storageConfig->getMountOptions(); + if (!empty($mountOptions)) { + $options['mountOptions'] = $mountOptions; } $mountPoints[$mountType][$applicable][$rootMountPoint] = $options; diff --git a/apps/files_sharing/api/local.php b/apps/files_sharing/api/local.php index 571982d153b..1a5edbfd070 100644 --- a/apps/files_sharing/api/local.php +++ b/apps/files_sharing/api/local.php @@ -343,7 +343,7 @@ class Local { if(isset($params['_put']['permissions'])) { return self::updatePermissions($share, $params); } elseif (isset($params['_put']['password'])) { - return self::updatePassword($share, $params); + return self::updatePassword($params['id'], (int)$share['share_type'], $params['_put']['password']); } elseif (isset($params['_put']['publicUpload'])) { return self::updatePublicUpload($share, $params); } elseif (isset($params['_put']['expireDate'])) { @@ -457,47 +457,22 @@ class Local { /** * update password for public link share - * @param array $share information about the share - * @param array $params 'password' + * @param int $shareId + * @param int $shareType + * @param string $password * @return \OC_OCS_Result */ - private static function updatePassword($share, $params) { - - $itemSource = $share['item_source']; - $itemType = $share['item_type']; - - if( (int)$share['share_type'] !== \OCP\Share::SHARE_TYPE_LINK) { + private static function updatePassword($shareId, $shareType, $password) { + if($shareType !== \OCP\Share::SHARE_TYPE_LINK) { return new \OC_OCS_Result(null, 400, "password protection is only supported for public shares"); } - $shareWith = isset($params['_put']['password']) ? $params['_put']['password'] : null; - - if($shareWith === '') { - $shareWith = null; - } - - $items = \OCP\Share::getItemShared($itemType, $itemSource); - - $checkExists = false; - foreach ($items as $item) { - if($item['share_type'] === \OCP\Share::SHARE_TYPE_LINK) { - $checkExists = true; - $permissions = $item['permissions']; - } - } - - if (!$checkExists) { - return new \OC_OCS_Result(null, 404, "share doesn't exists, can't change password"); + if($password === '') { + $password = null; } try { - $result = \OCP\Share::shareItem( - $itemType, - $itemSource, - \OCP\Share::SHARE_TYPE_LINK, - $shareWith, - $permissions - ); + $result = \OCP\Share::setPassword($shareId, $password); } catch (\Exception $e) { return new \OC_OCS_Result(null, 403, $e->getMessage()); } diff --git a/apps/files_sharing/appinfo/app.php b/apps/files_sharing/appinfo/app.php index a222973fc34..d009fbca3b9 100644 --- a/apps/files_sharing/appinfo/app.php +++ b/apps/files_sharing/appinfo/app.php @@ -51,6 +51,10 @@ OCP\Share::registerBackend('folder', 'OC_Share_Backend_Folder', 'file'); OCP\Util::addScript('files_sharing', 'share'); OCP\Util::addScript('files_sharing', 'external'); +// FIXME: registering a job here will cause additional useless SQL queries +// when the route is not cron.php, needs a better way +\OC::$server->getJobList()->add('OCA\Files_sharing\Lib\DeleteOrphanedSharesJob'); + \OC::$server->getActivityManager()->registerExtension(function() { return new \OCA\Files_Sharing\Activity( \OC::$server->query('L10NFactory'), diff --git a/apps/files_sharing/js/settings-admin.js b/apps/files_sharing/js/settings-admin.js index 257c864b04f..95578bff548 100644 --- a/apps/files_sharing/js/settings-admin.js +++ b/apps/files_sharing/js/settings-admin.js @@ -8,4 +8,5 @@ $(document).ready(function() { OC.AppConfig.setValue('files_sharing', $(this).attr('name'), value); }); + $('.section .icon-info').tipsy({gravity: 'w'}); }); diff --git a/apps/files_sharing/lib/deleteorphanedsharesjob.php b/apps/files_sharing/lib/deleteorphanedsharesjob.php new file mode 100644 index 00000000000..f39078b778f --- /dev/null +++ b/apps/files_sharing/lib/deleteorphanedsharesjob.php @@ -0,0 +1,59 @@ +<?php +/** + * @author Morris Jobke <hey@morrisjobke.de> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Files_sharing\Lib; + +use Doctrine\DBAL\Platforms\SqlitePlatform; +use OCP\IDBConnection; +use OC\BackgroundJob\TimedJob; + +/** + * Delete all share entries that have no matching entries in the file cache table. + */ +class DeleteOrphanedSharesJob extends TimedJob { + + /** + * Default interval in minutes + * + * @var int $defaultIntervalMin + **/ + protected $defaultIntervalMin = 15; + + /** + * Makes the background job do its work + * + * @param array $argument unused argument + */ + public function run($argument) { + $connection = \OC::$server->getDatabaseConnection(); + $logger = \OC::$server->getLogger(); + + $sql = + 'DELETE FROM `*PREFIX*share` ' . + 'WHERE `item_type` in (\'file\', \'folder\') ' . + 'AND NOT EXISTS (SELECT `fileid` FROM `*PREFIX*filecache` WHERE `file_source` = `fileid`)'; + + $deletedEntries = $connection->executeUpdate($sql); + $logger->info("$deletedEntries orphaned share(s) deleted", ['app' => 'DeleteOrphanedSharesJob']); + } + +} diff --git a/apps/files_sharing/lib/helper.php b/apps/files_sharing/lib/helper.php index 3f1de7233ae..5b5525e244f 100644 --- a/apps/files_sharing/lib/helper.php +++ b/apps/files_sharing/lib/helper.php @@ -33,7 +33,6 @@ class Helper { \OCP\Util::connectHook('OC_Filesystem', 'setup', '\OC\Files\Storage\Shared', 'setup'); \OCP\Util::connectHook('OC_Filesystem', 'setup', '\OCA\Files_Sharing\External\Manager', 'setup'); \OCP\Util::connectHook('OC_Filesystem', 'post_write', '\OC\Files\Cache\Shared_Updater', 'writeHook'); - \OCP\Util::connectHook('OC_Filesystem', 'post_delete', '\OC\Files\Cache\Shared_Updater', 'postDeleteHook'); \OCP\Util::connectHook('OC_Filesystem', 'delete', '\OC\Files\Cache\Shared_Updater', 'deleteHook'); \OCP\Util::connectHook('OC_Filesystem', 'post_rename', '\OC\Files\Cache\Shared_Updater', 'renameHook'); \OCP\Util::connectHook('OC_Filesystem', 'post_delete', '\OCA\Files_Sharing\Hooks', 'unshareChildren'); diff --git a/apps/files_sharing/lib/updater.php b/apps/files_sharing/lib/updater.php index d748d5555f6..322c031f2f1 100644 --- a/apps/files_sharing/lib/updater.php +++ b/apps/files_sharing/lib/updater.php @@ -27,9 +27,6 @@ namespace OC\Files\Cache; class Shared_Updater { - // shares which can be removed from oc_share after the delete operation was successful - static private $toRemove = array(); - /** * walk up the users file tree and update the etags * @param string $user @@ -82,25 +79,6 @@ class Shared_Updater { } /** - * remove all shares for a given file if the file was deleted - * - * @param string $path - */ - private static function removeShare($path) { - $fileSource = self::$toRemove[$path]; - - if (!\OC\Files\Filesystem::file_exists($path)) { - $query = \OC_DB::prepare('DELETE FROM `*PREFIX*share` WHERE `file_source`=?'); - try { - \OC_DB::executeAudited($query, array($fileSource)); - } catch (\Exception $e) { - \OCP\Util::writeLog('files_sharing', "can't remove share: " . $e->getMessage(), \OCP\Util::WARN); - } - } - unset(self::$toRemove[$path]); - } - - /** * @param array $params */ static public function writeHook($params) { @@ -122,19 +100,6 @@ class Shared_Updater { static public function deleteHook($params) { $path = $params['path']; self::correctFolders($path); - - $fileInfo = \OC\Files\Filesystem::getFileInfo($path); - - // mark file as deleted so that we can clean up the share table if - // the file was deleted successfully - self::$toRemove[$path] = $fileInfo['fileid']; - } - - /** - * @param array $params - */ - static public function postDeleteHook($params) { - self::removeShare($params['path']); } /** diff --git a/apps/files_sharing/templates/settings-admin.php b/apps/files_sharing/templates/settings-admin.php index bd803517597..376f2b71aee 100644 --- a/apps/files_sharing/templates/settings-admin.php +++ b/apps/files_sharing/templates/settings-admin.php @@ -4,6 +4,9 @@ ?> <div id="fileSharingSettings"> <h3><?php p($l->t('Federated Cloud Sharing'));?></h3> + <a target="_blank" class="icon-info svg" + title="<?php p($l->t('Open documentation'));?>" + href="<?php p(link_to_docs('admin-sharing-federated')); ?>"></a> <p> <input type="checkbox" name="outgoing_server2server_share_enabled" id="outgoingServer2serverShareEnabled" diff --git a/apps/files_sharing/tests/deleteorphanedsharesjobtest.php b/apps/files_sharing/tests/deleteorphanedsharesjobtest.php new file mode 100644 index 00000000000..20f3bcd5ebd --- /dev/null +++ b/apps/files_sharing/tests/deleteorphanedsharesjobtest.php @@ -0,0 +1,162 @@ +<?php +/** + * @author Morris Jobke <hey@morrisjobke.de> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace Test\BackgroundJob; + +use OCA\Files_sharing\Lib\DeleteOrphanedSharesJob; + +class DeleteOrphanedSharesJobTest extends \Test\TestCase { + + /** + * @var bool + */ + private static $trashBinStatus; + + /** + * @var DeleteOrphanedSharesJob + */ + private $job; + + /** + * @var \OCP\IDBConnection + */ + private $connection; + + /** + * @var string + */ + private $user1; + + /** + * @var string + */ + private $user2; + + public static function setUpBeforeClass() { + $appManager = \OC::$server->getAppManager(); + self::$trashBinStatus = $appManager->isEnabledForUser('files_trashbin'); + $appManager->disableApp('files_trashbin'); + + // just in case... + \OC\Files\Filesystem::getLoader()->removeStorageWrapper('oc_trashbin'); + } + + public static function tearDownAfterClass() { + if (self::$trashBinStatus) { + \OC::$server->getAppManager()->enableApp('files_trashbin'); + } + } + + protected function setup() { + parent::setUp(); + + $this->connection = \OC::$server->getDatabaseConnection(); + + $this->user1 = $this->getUniqueID('user1_'); + $this->user2 = $this->getUniqueID('user2_'); + + $userManager = \OC::$server->getUserManager(); + $userManager->createUser($this->user1, 'pass'); + $userManager->createUser($this->user2, 'pass'); + + \OC::registerShareHooks(); + + $this->job = new DeleteOrphanedSharesJob(); + } + + protected function tearDown() { + $this->connection->executeUpdate('DELETE FROM `*PREFIX*share` WHERE `item_type` in (\'file\', \'folder\')'); + + $userManager = \OC::$server->getUserManager(); + $user1 = $userManager->get($this->user1); + if($user1) { + $user1->delete(); + } + $user2 = $userManager->get($this->user2); + if($user2) { + $user2->delete(); + } + + $this->logout(); + + parent::tearDown(); + } + + private function getShares() { + $shares = []; + $result = $this->connection->executeQuery('SELECT * FROM `*PREFIX*share`'); + while ($row = $result->fetch()) { + $shares[] = $row; + } + $result->closeCursor(); + return $shares; + } + + /** + * Test clearing orphaned shares + */ + public function testClearShares() { + $this->loginAsUser($this->user1); + + $view = new \OC\Files\View('/' . $this->user1 . '/'); + $view->mkdir('files/test'); + $view->mkdir('files/test/sub'); + + $fileInfo = $view->getFileInfo('files/test/sub'); + $fileId = $fileInfo->getId(); + + $this->assertTrue( + \OCP\Share::shareItem('folder', $fileId, \OCP\Share::SHARE_TYPE_USER, $this->user2, \OCP\Constants::PERMISSION_READ), + 'Failed asserting that user 1 successfully shared "test/sub" with user 2.' + ); + + $this->assertCount(1, $this->getShares()); + + $this->job->run([]); + + $this->assertCount(1, $this->getShares(), 'Linked shares not deleted'); + + $view->unlink('files/test'); + + $this->job->run([]); + + $this->assertCount(0, $this->getShares(), 'Orphaned shares deleted'); + } + + public function testKeepNonFileShares() { + $this->loginAsUser($this->user1); + + \OCP\Share::registerBackend('test', 'Test_Share_Backend'); + + $this->assertTrue( + \OCP\Share::shareItem('test', 'test.txt', \OCP\Share::SHARE_TYPE_USER, $this->user2, \OCP\Constants::PERMISSION_READ), + 'Failed asserting that user 1 successfully shared something with user 2.' + ); + + $this->assertCount(1, $this->getShares()); + + $this->job->run([]); + + $this->assertCount(1, $this->getShares(), 'Non-file shares kept'); + } +} + diff --git a/apps/files_trashbin/tests/storage.php b/apps/files_trashbin/tests/storage.php index f1ac055d335..d5bd7c318d3 100644 --- a/apps/files_trashbin/tests/storage.php +++ b/apps/files_trashbin/tests/storage.php @@ -35,11 +35,6 @@ class Storage extends \Test\TestCase { private $user; /** - * @var \OC\Files\Storage\Storage - **/ - private $originalStorage; - - /** * @var \OC\Files\View */ private $rootView; @@ -61,8 +56,6 @@ class Storage extends \Test\TestCase { // this will setup the FS $this->loginAsUser($this->user); - $this->originalStorage = \OC\Files\Filesystem::getStorage('/'); - \OCA\Files_Trashbin\Storage::setupStorage(); $this->rootView = new \OC\Files\View('/'); @@ -73,7 +66,6 @@ class Storage extends \Test\TestCase { protected function tearDown() { \OC\Files\Filesystem::getLoader()->removeStorageWrapper('oc_trashbin'); - \OC\Files\Filesystem::mount($this->originalStorage, array(), '/'); $this->logout(); \OC_User::deleteUser($this->user); \OC_Hook::clear(); diff --git a/apps/user_ldap/ajax/getNewServerConfigPrefix.php b/apps/user_ldap/ajax/getNewServerConfigPrefix.php index 7d1434a8fb4..d0135917886 100644 --- a/apps/user_ldap/ajax/getNewServerConfigPrefix.php +++ b/apps/user_ldap/ajax/getNewServerConfigPrefix.php @@ -32,4 +32,17 @@ sort($serverConnections); $lk = array_pop($serverConnections); $ln = intval(str_replace('s', '', $lk)); $nk = 's'.str_pad($ln+1, 2, '0', STR_PAD_LEFT); -OCP\JSON::success(array('configPrefix' => $nk)); + +$resultData = array('configPrefix' => $nk); + +if(isset($_POST['copyConfig'])) { + $originalConfig = new \OCA\user_ldap\lib\Configuration($_POST['copyConfig']); + $newConfig = new \OCA\user_ldap\lib\Configuration($nk, false); + $newConfig->setConfiguration($originalConfig->getConfiguration()); + $newConfig->saveConfiguration(); +} else { + $configuration = new \OCA\user_ldap\lib\Configuration($nk, false); + $resultData['defaults'] = $configuration->getDefaults(); +} + +OCP\JSON::success($resultData); diff --git a/apps/user_ldap/ajax/wizard.php b/apps/user_ldap/ajax/wizard.php index ab521b5bf0e..267b9568a28 100644 --- a/apps/user_ldap/ajax/wizard.php +++ b/apps/user_ldap/ajax/wizard.php @@ -72,13 +72,11 @@ switch($action) { case 'determineGroupsForGroups': case 'determineAttributes': case 'getUserListFilter': - case 'getLoginFilterMode': case 'getUserLoginFilter': - case 'getUserFilterMode': case 'getGroupFilter': - case 'getGroupFilterMode': case 'countUsers': case 'countGroups': + case 'countInBaseDN': try { $result = $wizard->$action(); if($result !== false) { @@ -93,6 +91,23 @@ switch($action) { exit; break; + case 'testLoginName': { + try { + $loginName = $_POST['ldap_test_loginname']; + $result = $wizard->$action($loginName); + if($result !== false) { + OCP\JSON::success($result->getResultArray()); + exit; + } + } catch (\Exception $e) { + \OCP\JSON::error(array('message' => $e->getMessage())); + exit; + } + \OCP\JSON::error(); + exit; + break; + } + case 'save': $key = isset($_POST['cfgkey']) ? $_POST['cfgkey'] : false; $val = isset($_POST['cfgval']) ? $_POST['cfgval'] : null; @@ -115,6 +130,6 @@ switch($action) { OCP\JSON::success(); break; default: - //TODO: return 4xx error + \OCP\JSON::error(array('message' => $l->t('Action does not exist'))); break; } diff --git a/apps/user_ldap/appinfo/version b/apps/user_ldap/appinfo/version index 8f0916f768f..a918a2aa18d 100644 --- a/apps/user_ldap/appinfo/version +++ b/apps/user_ldap/appinfo/version @@ -1 +1 @@ -0.5.0 +0.6.0 diff --git a/apps/user_ldap/css/settings.css b/apps/user_ldap/css/settings.css index 8f339451c64..b351f9ae2af 100644 --- a/apps/user_ldap/css/settings.css +++ b/apps/user_ldap/css/settings.css @@ -1,6 +1,6 @@ .table { display: table; - width: 60%; + width: 85%; } .tablerow { @@ -21,10 +21,18 @@ margin-left: 3px; } +.ldapIconCopy { + background-image: url('../img/copy.svg'); +} + .invisible { visibility: hidden; } +.forceHidden { + display: none !important; +} + .ldapSettingsTabs { float: right !important; } @@ -49,13 +57,16 @@ } #ldapWizard1 .hostPortCombinator div span { - width: 7%; - display: table-cell; + width: 14.5%; + display: inline-block; text-align: right; } #ldapWizard1 .host { - width: 96.5% !important; + width: 100%; + margin-left: 0; + margin-right: 0; + border: 0; } .tableCellInput { @@ -77,7 +88,7 @@ color: #FF3B3B; } -.wizSpinner { +.ldapSpinner { height: 15px; margin: 5px; } @@ -104,10 +115,51 @@ width: auto; } +.ldapManyGroupsSupport span { + display: inline-block; + vertical-align: top; + height: 150px; +} + +.ldapManyGroupsSupport span button { + margin-top: 35px; +} + +.ldapManyGroupsSearch { + width: 425px !important; +} + +.ldapGroupList { + height: 150px; + width: 200px; +} + #ldap fieldset input, #ldap fieldset textarea { width: 60%; } +#ldap fieldset textarea ~ button { + vertical-align: text-bottom; +} + +input.ldapVerifyInput { + width: 150px !important; +} + +.ldapInputColElement { + width: 35%; + display: inline-block; + padding-left: 10px; +} + +.ldapToggle { + text-decoration: underline; +} + +span.ldapInputColElement { + margin-top: 9px; +} + #ldap fieldset p input[type=checkbox] { vertical-align: bottom; } diff --git a/apps/user_ldap/img/copy.png b/apps/user_ldap/img/copy.png Binary files differnew file mode 100644 index 00000000000..283d627a5a7 --- /dev/null +++ b/apps/user_ldap/img/copy.png diff --git a/apps/user_ldap/img/copy.svg b/apps/user_ldap/img/copy.svg new file mode 100644 index 00000000000..2e19d8066e3 --- /dev/null +++ b/apps/user_ldap/img/copy.svg @@ -0,0 +1,120 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.w3.org/2000/svg" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:ns1="http://sozi.baierouge.fr" + xmlns:cc="http://web.resource.org/cc/" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + id="svg1" + sodipodi:docname="copy.svg" + viewBox="0 0 60 60" + sodipodi:version="0.32" + _SVGFile__filename="oldscale/actions/copy.svg" + version="1.0" + y="0" + x="0" + inkscape:version="0.40" + sodipodi:docbase="/home/danny/work/flat/SVG/mono/scalable/actions" + > + <sodipodi:namedview + id="base" + bordercolor="#666666" + inkscape:pageshadow="2" + inkscape:window-y="0" + pagecolor="#ffffff" + inkscape:window-height="699" + inkscape:zoom="4.9119411" + inkscape:window-x="0" + borderopacity="1.0" + inkscape:current-layer="svg1" + inkscape:cx="60.290892" + inkscape:cy="24.030855" + inkscape:window-width="1024" + inkscape:pageopacity="0.0" + /> + <path + id="path1716" + style="stroke-linejoin:round;stroke:#ffffff;stroke-width:8.3605;fill:none" + transform="matrix(.97183 0 0 .97183 .87037 .23733)" + d="m7.513 4.5767c-1.3709 0-2.4745 1.1036-2.4745 2.4745v31.564c0 1.371 1.1036 2.474 2.4745 2.474h29.783c1.371 0 2.474-1.103 2.474-2.474v-31.564c0-1.3707-1.103-2.4743-2.474-2.4743h-29.783z" + /> + <path + id="rect1101" + style="stroke-linejoin:round;fill-rule:evenodd;stroke:#000000;stroke-width:3.2156;fill:#ffffff" + transform="matrix(.97183 0 0 .97183 .87037 .23733)" + d="m7.513 4.5767c-1.3709 0-2.4745 1.1036-2.4745 2.4745v31.564c0 1.371 1.1036 2.474 2.4745 2.474h29.783c1.371 0 2.474-1.103 2.474-2.474v-31.564c0-1.3707-1.103-2.4743-2.474-2.4743h-29.783z" + /> + <path + id="path1094" + style="stroke-linejoin:round;stroke:#ffffff;stroke-width:8.3605;fill:none" + transform="matrix(.97183 0 0 .97183 .87037 .23733)" + d="m22.652 20.161c-1.37 0-2.474 1.104-2.474 2.475v31.564c0 1.371 1.104 2.474 2.474 2.474h29.783c1.371 0 2.475-1.103 2.475-2.474v-31.564c0-1.371-1.104-2.475-2.475-2.475h-29.783z" + /> + <path + id="rect1111" + style="stroke-linejoin:round;fill-rule:evenodd;stroke:#000000;stroke-width:3.2156;fill:#ffffff" + transform="matrix(.97183 0 0 .97183 .87037 .23733)" + d="m22.652 20.161c-1.37 0-2.474 1.104-2.474 2.475v31.564c0 1.371 1.104 2.474 2.474 2.474h29.783c1.371 0 2.475-1.103 2.475-2.474v-31.564c0-1.371-1.104-2.475-2.475-2.475h-29.783z" + /> + <path + id="path1718" + style="stroke-linejoin:round;stroke:#ffffff;stroke-linecap:round;stroke-width:8.125;fill:none" + d="m12.457 21.599c2.325 20.529 20.15 19.15 21.296 19.043v6.139l8.981-8.889-8.981-8.87v6.066c-1.348 0.159-13.941 1.422-21.296-13.489z" + /> + <path + id="path1574" + style="stroke-linejoin:round;fill-rule:evenodd;stroke:#000000;stroke-linecap:round;stroke-width:3.125;fill:#000000" + d="m12.457 21.599c2.325 20.529 20.15 19.15 21.296 19.043v6.139l8.981-8.889-8.981-8.87v6.066c-1.348 0.159-13.941 1.422-21.296-13.489z" + /> + <metadata + > + <rdf:RDF + > + <cc:Work + > + <dc:format + >image/svg+xml</dc:format + > + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" + /> + <cc:license + rdf:resource="http://creativecommons.org/licenses/publicdomain/" + /> + <dc:publisher + > + <cc:Agent + rdf:about="http://openclipart.org/" + > + <dc:title + >Openclipart</dc:title + > + </cc:Agent + > + </dc:publisher + > + </cc:Work + > + <cc:License + rdf:about="http://creativecommons.org/licenses/publicdomain/" + > + <cc:permits + rdf:resource="http://creativecommons.org/ns#Reproduction" + /> + <cc:permits + rdf:resource="http://creativecommons.org/ns#Distribution" + /> + <cc:permits + rdf:resource="http://creativecommons.org/ns#DerivativeWorks" + /> + </cc:License + > + </rdf:RDF + > + </metadata + > +</svg +> diff --git a/apps/user_ldap/js/experiencedAdmin.js b/apps/user_ldap/js/experiencedAdmin.js deleted file mode 100644 index 7dc5a4e503d..00000000000 --- a/apps/user_ldap/js/experiencedAdmin.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright (c) 2014, Arthur Schiwon <blizzz@owncloud.com> - * This file is licensed under the Affero General Public License version 3 or later. - * See the COPYING-README file. - */ - -/* global LdapWizard */ - -/** - * controls behaviour depend on whether the admin is experienced in LDAP or not. - * - * @class - * @param {object} wizard the LDAP Wizard object - * @param {boolean} initialState whether the admin is experienced or not - */ -function ExperiencedAdmin(wizard, initialState) { - this.wizard = wizard; - this._isExperienced = initialState; - if(this._isExperienced) { - this.hideEntryCounters(); - } -} - - -/** - * toggles whether the admin is an experienced one or not - * - * @param {boolean} isExperienced whether the admin is experienced or not - */ -ExperiencedAdmin.prototype.setExperienced = function(isExperienced) { - this._isExperienced = isExperienced; - if(this._isExperienced) { - this.enableRawMode(); - this.hideEntryCounters(); - } else { - this.showEntryCounters(); - } -}; - -/** -* answers whether the admin is an experienced one or not -* -* @return {boolean} whether the admin is experienced or not -*/ -ExperiencedAdmin.prototype.isExperienced = function() { - return this._isExperienced; -}; - -/** - * switches all LDAP filters from Assisted to Raw mode. - */ -ExperiencedAdmin.prototype.enableRawMode = function() { - LdapWizard._save({id: 'ldapGroupFilterMode'}, LdapWizard.filterModeRaw); - LdapWizard._save({id: 'ldapUserFilterMode' }, LdapWizard.filterModeRaw); - LdapWizard._save({id: 'ldapLoginFilterMode'}, LdapWizard.filterModeRaw); -}; - -ExperiencedAdmin.prototype.updateUserTab = function(mode) { - this._updateTab(mode, $('#ldap_user_count')); -}; - -ExperiencedAdmin.prototype.updateGroupTab = function(mode) { - this._updateTab(mode, $('#ldap_group_count')); -}; - -ExperiencedAdmin.prototype._updateTab = function(mode, $countEl) { - if(mode === LdapWizard.filterModeAssisted) { - $countEl.removeClass('hidden'); - } else if(!this._isExperienced) { - $countEl.removeClass('hidden'); - } else { - $countEl.addClass('hidden'); - } -}; - -/** - * hide user and group counters, they will be displayed on demand only - */ -ExperiencedAdmin.prototype.hideEntryCounters = function() { - $('#ldap_user_count').addClass('hidden'); - $('#ldap_group_count').addClass('hidden'); - $('.ldapGetEntryCount').removeClass('hidden'); -}; - -/** -* shows user and group counters, they will be displayed on demand only -*/ -ExperiencedAdmin.prototype.showEntryCounters = function() { - $('#ldap_user_count').removeClass('hidden'); - $('#ldap_group_count').removeClass('hidden'); - $('.ldapGetEntryCount').addClass('hidden'); -}; diff --git a/apps/user_ldap/js/ldapFilter.js b/apps/user_ldap/js/ldapFilter.js deleted file mode 100644 index dc65858217d..00000000000 --- a/apps/user_ldap/js/ldapFilter.js +++ /dev/null @@ -1,193 +0,0 @@ -/* global LdapWizard */ - -function LdapFilter(target, determineModeCallback) { - this.locked = true; - this.target = false; - this.mode = LdapWizard.filterModeAssisted; - this.lazyRunCompose = false; - this.determineModeCallback = determineModeCallback; - this.foundFeatures = false; - this.activated = false; - this.countPending = false; - - if( target === 'User' || - target === 'Login' || - target === 'Group') { - this.target = target; - } -} - -LdapFilter.prototype.activate = function() { - if(this.activated) { - // might be necessary, if configuration changes happened. - this.findFeatures(); - return; - } - this.activated = true; - - this.determineMode(); -}; - -LdapFilter.prototype.compose = function(updateCount) { - var action; - - if(updateCount === true) { - this.countPending = updateCount; - } - - if(this.locked) { - this.lazyRunCompose = true; - return false; - } - - if(this.mode === LdapWizard.filterModeRaw) { - //Raw filter editing, i.e. user defined filter, don't compose - return; - } - - if(this.target === 'User') { - action = 'getUserListFilter'; - } else if(this.target === 'Login') { - action = 'getUserLoginFilter'; - } else if(this.target === 'Group') { - action = 'getGroupFilter'; - } - - var param = 'action='+action+ - '&ldap_serverconfig_chooser='+ - encodeURIComponent($('#ldap_serverconfig_chooser').val()); - - var filter = this; - - LdapWizard.ajax(param, - function(result) { - filter.afterComposeSuccess(result); - }, - function () { - filter.countPending = false; - console.log('LDAP Wizard: could not compose filter. '+ - 'Please check owncloud.log'); - } - ); -}; - -/** - * this function is triggered after LDAP filters have been composed successfully - * @param {object} result returned by the ajax call - */ -LdapFilter.prototype.afterComposeSuccess = function(result) { - LdapWizard.applyChanges(result); - if(this.countPending) { - this.countPending = false; - this.updateCount(); - } -}; - -LdapFilter.prototype.determineMode = function() { - var param = 'action=get'+encodeURIComponent(this.target)+'FilterMode'+ - '&ldap_serverconfig_chooser='+ - encodeURIComponent($('#ldap_serverconfig_chooser').val()); - - var filter = this; - LdapWizard.ajax(param, - function(result) { - var property = 'ldap' + filter.target + 'FilterMode'; - filter.mode = parseInt(result.changes[property], 10); - var rawContainerIsInvisible = - $('#raw'+filter.target+'FilterContainer').hasClass('invisible'); - if ( filter.mode === LdapWizard.filterModeRaw - && rawContainerIsInvisible - ) { - LdapWizard['toggleRaw'+filter.target+'Filter'](); - } else if ( filter.mode === LdapWizard.filterModeAssisted - && !rawContainerIsInvisible - ) { - LdapWizard['toggleRaw'+filter.target+'Filter'](); - } else { - console.log('LDAP Wizard determineMode: returned mode was »' + - filter.mode + '« of type ' + typeof filter.mode); - } - filter.unlock(); - filter.determineModeCallback(filter.mode); - }, - function () { - //on error case get back to default i.e. Assisted - if(!$('#raw'+filter.target+'FilterContainer').hasClass('invisible')) { - LdapWizard['toggleRaw'+filter.target+'Filter'](); - filter.mode = LdapWizard.filterModeAssisted; - } - filter.unlock(); - filter.determineModeCallback(filter.mode); - } - ); -}; - -LdapFilter.prototype.setMode = function(mode) { - if(mode === LdapWizard.filterModeAssisted || mode === LdapWizard.filterModeRaw) { - this.mode = mode; - } -}; - -LdapFilter.prototype.getMode = function() { - return this.mode; -}; - -LdapFilter.prototype.unlock = function() { - this.locked = false; - if(this.lazyRunCompose) { - this.lazyRunCompose = false; - this.compose(); - } -}; - -/** - * resets this.foundFeatures so that LDAP queries can be fired again to retrieve - * objectClasses, groups, etc. - */ -LdapFilter.prototype.reAllowFeatureLookup = function () { - this.foundFeatures = false; -}; - -LdapFilter.prototype.findFeatures = function() { - if(!this.foundFeatures && !this.locked && this.mode === LdapWizard.filterModeAssisted) { - this.foundFeatures = true; - var objcEl, avgrEl; - if(this.target === 'User') { - objcEl = 'ldap_userfilter_objectclass'; - avgrEl = 'ldap_userfilter_groups'; - } else if (this.target === 'Group') { - objcEl = 'ldap_groupfilter_objectclass'; - avgrEl = 'ldap_groupfilter_groups'; - } else if (this.target === 'Login') { - LdapWizard.findAttributes(); - return; - } else { - return false; - } - LdapWizard.findObjectClasses(objcEl, this.target); - LdapWizard.findAvailableGroups(avgrEl, this.target + "s"); - } -}; - -/** - * this function is triggered before user and group counts are executed - * resolving the passed status variable will fire up counting - */ -LdapFilter.prototype.beforeUpdateCount = function() { - var status = $.Deferred(); - LdapWizard.runDetectors(this.target, function() { - status.resolve(); - }); - return status; -}; - -LdapFilter.prototype.updateCount = function(doneCallback) { - var filter = this; - $.when(this.beforeUpdateCount()).done(function() { - if(filter.target === 'User') { - LdapWizard.countUsers(doneCallback); - } else if (filter.target === 'Group') { - LdapWizard.countGroups(doneCallback); - } - }); -}; diff --git a/apps/user_ldap/js/settings.js b/apps/user_ldap/js/settings.js deleted file mode 100644 index 768d62a18d1..00000000000 --- a/apps/user_ldap/js/settings.js +++ /dev/null @@ -1,1205 +0,0 @@ -var LdapConfiguration = { - refreshConfig: function() { - if($('#ldap_serverconfig_chooser option').length < 2) { - LdapConfiguration.addConfiguration(true); - return; - } - $.post( - OC.filePath('user_ldap','ajax','getConfiguration.php'), - $('#ldap_serverconfig_chooser').serialize(), - function (result) { - if(result.status === 'success') { - $.each(result.configuration, function(configkey, configvalue) { - elementID = '#'+configkey; - - //deal with Checkboxes - if($(elementID).is('input[type=checkbox]')) { - if(parseInt(configvalue, 10) === 1) { - $(elementID).attr('checked', 'checked'); - } else { - $(elementID).removeAttr('checked'); - } - return; - } - - //On Textareas, Multi-Line Settings come as array - if($(elementID).is('textarea') && $.isArray(configvalue)) { - configvalue = configvalue.join("\n"); - } - - // assign the value - $('#'+configkey).val(configvalue); - }); - LdapWizard.init(); - } - } - ); - }, - - resetDefaults: function() { - $('#ldap').find('input[type=text], input[type=number], input[type=password], textarea, select').each(function() { - if($(this).attr('id') === 'ldap_serverconfig_chooser') { - return; - } - $(this).val($(this).attr('data-default')); - }); - $('#ldap').find('input[type=checkbox]').each(function() { - if($(this).attr('data-default') === 1) { - $(this).attr('checked', 'checked'); - } else { - $(this).removeAttr('checked'); - } - }); - }, - - deleteConfiguration: function() { - $.post( - OC.filePath('user_ldap','ajax','deleteConfiguration.php'), - $('#ldap_serverconfig_chooser').serialize(), - function (result) { - if(result.status === 'success') { - $('#ldap_serverconfig_chooser option:selected').remove(); - $('#ldap_serverconfig_chooser option:first').select(); - LdapConfiguration.refreshConfig(); - } else { - OC.dialogs.alert( - result.message, - t('user_ldap', 'Deletion failed') - ); - } - } - ); - }, - - addConfiguration: function(doNotAsk) { - $.post( - OC.filePath('user_ldap','ajax','getNewServerConfigPrefix.php'), - function (result) { - if(result.status === 'success') { - if(doNotAsk) { - LdapConfiguration.resetDefaults(); - } else { - OC.dialogs.confirm( - t('user_ldap', 'Take over settings from recent server configuration?'), - t('user_ldap', 'Keep settings?'), - function(keep) { - if(!keep) { - LdapConfiguration.resetDefaults(); - } - } - ); - } - $('#ldap_serverconfig_chooser option:selected').removeAttr('selected'); - var html = '<option value="'+result.configPrefix+'" selected="selected">'+t('user_ldap','{nthServer}. Server', {nthServer: $('#ldap_serverconfig_chooser option').length})+'</option>'; - $('#ldap_serverconfig_chooser option:last').before(html); - LdapWizard.init(); - } else { - OC.dialogs.alert( - result.message, - t('user_ldap', 'Cannot add server configuration') - ); - } - } - ); - }, - - testConfiguration: function(onSuccess, onError) { - $.post( - OC.filePath('user_ldap','ajax','testConfiguration.php'), - $('#ldap').serialize(), - function (result) { - if (result.status === 'success') { - onSuccess(result); - } else { - onError(result); - } - } - ); - }, - - clearMappings: function(mappingSubject) { - $.post( - OC.filePath('user_ldap','ajax','clearMappings.php'), - 'ldap_clear_mapping='+encodeURIComponent(mappingSubject), - function(result) { - if(result.status === 'success') { - OC.dialogs.info( - t('user_ldap', 'mappings cleared'), - t('user_ldap', 'Success') - ); - } else { - OC.dialogs.alert( - result.message, - t('user_ldap', 'Error') - ); - } - } - ); - } -}; - -var LdapWizard = { - checkPortInfoShown: false, - saveBlacklist: {}, - userFilterGroupSelectState: 'enable', - spinner: '<img class="wizSpinner" src="'+ OC.imagePath('core', 'loading.gif') +'">', - filterModeAssisted: 0, - filterModeRaw: 1, - userFilter: false, - loginFilter: false, - groupFilter: false, - ajaxRequests: {}, - lastTestSuccessful: true, - - ajax: function(param, fnOnSuccess, fnOnError, reqID) { - if(!_.isUndefined(reqID)) { - if(LdapWizard.ajaxRequests.hasOwnProperty(reqID)) { - console.log('aborting ' + reqID); - console.log(param); - LdapWizard.ajaxRequests[reqID].abort(); - } - } - var request = $.post( - OC.filePath('user_ldap','ajax','wizard.php'), - param, - function(result) { - if(result.status === 'success') { - fnOnSuccess(result); - } else { - fnOnError(result); - } - } - ); - if(!_.isUndefined(reqID)) { - LdapWizard.ajaxRequests[reqID] = request; - } - return request; - }, - - applyChanges: function (result) { - for (var id in result.changes) { - LdapWizard.blacklistAdd(id); - if(id.indexOf('count') > 0) { - $('#'+id).text(result.changes[id]); - } else { - $('#'+id).val(result.changes[id]); - } - } - LdapWizard.functionalityCheck(); - - if($('#ldapSettings').tabs('option', 'active') == 0) { - LdapWizard.basicStatusCheck(); - } - }, - - enableTabs: function() { - //do not use this function directly, use basicStatusCheck instead. - if(LdapWizard.saveProcesses === 0) { - $('.ldap_action_continue').removeAttr('disabled'); - $('.ldap_action_back').removeAttr('disabled'); - $('#ldapSettings').tabs('option', 'disabled', []); - } - }, - - disableTabs: function() { - $('.ldap_action_continue').attr('disabled', 'disabled'); - $('.ldap_action_back').attr('disabled', 'disabled'); - $('#ldapSettings').tabs('option', 'disabled', [1, 2, 3, 4, 5]); - }, - - basicStatusCheck: function() { - //criteria to continue from the first tab - // - host, port, user filter, agent dn, password, base dn - var host = $('#ldap_host').val(); - var port = $('#ldap_port').val(); - var agent = $('#ldap_dn').val(); - var pwd = $('#ldap_agent_password').val(); - var base = $('#ldap_base').val(); - - if((host && port && base) && ((!agent && !pwd) || (agent && pwd))) { - LdapWizard.enableTabs(); - } else { - LdapWizard.disableTabs(); - } - }, - - - blacklistAdd: function(id) { - var obj = $('#' + id); - if(!(obj[0].hasOwnProperty('multiple') && obj[0]['multiple'] === true)) { - //no need to blacklist multiselect - LdapWizard.saveBlacklist[id] = true; - return true; - } - return false; - }, - - blacklistRemove: function(id) { - if(LdapWizard.saveBlacklist.hasOwnProperty(id)) { - delete LdapWizard.saveBlacklist[id]; - return true; - } - return false; - }, - - checkBaseDN: function() { - var host = $('#ldap_host').val(); - var port = $('#ldap_port').val(); - var user = $('#ldap_dn').val(); - var pass = $('#ldap_agent_password').val(); - - //FIXME: determine base dn with anonymous access - if(host && port && user && pass) { - var param = 'action=guessBaseDN'+ - '&ldap_serverconfig_chooser='+ - encodeURIComponent($('#ldap_serverconfig_chooser').val()); - - LdapWizard.showSpinner('#ldap_base'); - $('#ldap_base').prop('disabled', 'disabled'); - LdapWizard.ajax(param, - function(result) { - LdapWizard.applyChanges(result); - LdapWizard.hideSpinner('#ldap_base'); - if($('#ldap_base').val()) { - LdapWizard.hideInfoBox(); - } - $('#ldap_base').prop('disabled', false); - }, - function (result) { - LdapWizard.hideSpinner('#ldap_base'); - LdapWizard.showInfoBox(t('user_ldap', 'Please specify a Base DN')); - LdapWizard.showInfoBox(t('user_ldap', 'Could not determine Base DN')); - $('#ldap_base').prop('disabled', false); - }, - 'guessBaseDN' - ); - } - }, - - checkPort: function() { - var host = $('#ldap_host').val(); - var port = $('#ldap_port').val(); - - if(host && !port) { - var param = 'action=guessPortAndTLS'+ - '&ldap_serverconfig_chooser='+ - encodeURIComponent($('#ldap_serverconfig_chooser').val()); - - LdapWizard.showSpinner('#ldap_port'); - $('#ldap_port').prop('disabled', 'disabled'); - LdapWizard.ajax(param, - function(result) { - LdapWizard.applyChanges(result); - LdapWizard.hideSpinner('#ldap_port'); - if($('#ldap_port').val()) { - LdapWizard.checkBaseDN(); - $('#ldap_port').prop('disabled', false); - LdapWizard.hideInfoBox(); - } - }, - function (result) { - LdapWizard.hideSpinner('#ldap_port'); - $('#ldap_port').prop('disabled', false); - LdapWizard.showInfoBox(t('user_ldap', 'Please specify the port')); - }, - 'guessPortAndTLS' - ); - } - }, - - controlBack: function() { - var curTabIndex = $('#ldapSettings').tabs('option', 'active'); - if(curTabIndex == 0) { - return; - } - $('#ldapSettings').tabs('option', 'active', curTabIndex - 1); - LdapWizard.controlUpdate(curTabIndex - 1); - }, - - controlContinue: function() { - var curTabIndex = $('#ldapSettings').tabs('option', 'active'); - if(curTabIndex == 3) { - return; - } - $('#ldapSettings').tabs('option', 'active', 1 + curTabIndex); - LdapWizard.controlUpdate(curTabIndex + 1); - }, - - controlUpdate: function(nextTabIndex) { - if(nextTabIndex == 0) { - $('.ldap_action_back').addClass('invisible'); - $('.ldap_action_continue').removeClass('invisible'); - } else - if(nextTabIndex == 1) { - $('.ldap_action_back').removeClass('invisible'); - $('.ldap_action_continue').removeClass('invisible'); - } else - if(nextTabIndex == 2) { - $('.ldap_action_continue').removeClass('invisible'); - $('.ldap_action_back').removeClass('invisible'); - } else - if(nextTabIndex == 3) { - //now last tab - $('.ldap_action_back').removeClass('invisible'); - $('.ldap_action_continue').addClass('invisible'); - } - }, - - _countThings: function(method, spinnerID, doneCallback) { - var param = 'action='+method+ - '&ldap_serverconfig_chooser='+ - encodeURIComponent($('#ldap_serverconfig_chooser').val()); - - LdapWizard.showSpinner(spinnerID); - LdapWizard.ajax(param, - function(result) { - LdapWizard.applyChanges(result); - LdapWizard.hideSpinner(spinnerID); - if(!_.isUndefined(doneCallback)) { - doneCallback(method); - } - }, - function (result) { - OC.Notification.showTemporary('Counting the entries failed with: ' + result.message); - LdapWizard.hideSpinner(spinnerID); - if(!_.isUndefined(doneCallback)) { - doneCallback(method); - } - }, - method - ); - }, - - countGroups: function(doneCallback) { - var groupFilter = $('#ldap_group_filter').val(); - if(!_.isEmpty(groupFilter)) { - LdapWizard._countThings('countGroups', '#ldap_group_count', doneCallback); - } - }, - - countUsers: function(doneCallback) { - var userFilter = $('#ldap_userlist_filter').val(); - if(!_.isEmpty(userFilter)) { - LdapWizard._countThings('countUsers', '#ldap_user_count', doneCallback); - } - }, - - /** - * called after detectors have run - * @callback runDetectorsCallback - */ - - /** - * runs detectors to determine appropriate attributes, e.g. displayName - * @param {string} type either "User" or "Group" - * @param {runDetectorsCallback} triggered after all detectors have completed - */ - runDetectors: function(type, callback) { - if(type === 'Group') { - $.when(LdapWizard.detectGroupMemberAssoc()) - .then(callback, callback); - if( LdapWizard.admin.isExperienced - && !(LdapWizard.detectorsRunInXPMode & LdapWizard.groupDetectors)) { - LdapWizard.detectorsRunInXPMode += LdapWizard.groupDetectors; - } - } else if(type === 'User') { - var req1 = LdapWizard.detectUserDisplayNameAttribute(); - var req2 = LdapWizard.detectEmailAttribute(); - $.when(req1, req2) - .then(callback, callback); - if( LdapWizard.admin.isExperienced - && !(LdapWizard.detectorsRunInXPMode & LdapWizard.userDetectors)) { - LdapWizard.detectorsRunInXPMode += LdapWizard.userDetectors; - } - } - }, - - /** - * runs detector to find out a fitting user display name attribute - */ - detectUserDisplayNameAttribute: function() { - var param = 'action=detectUserDisplayNameAttribute' + - '&ldap_serverconfig_chooser='+ - encodeURIComponent($('#ldap_serverconfig_chooser').val()); - - //runs in the background, no callbacks necessary - return LdapWizard.ajax(param, LdapWizard.applyChanges, function(){}, 'detectUserDisplayNameAttribute'); - }, - - detectEmailAttribute: function() { - var param = 'action=detectEmailAttribute'+ - '&ldap_serverconfig_chooser='+ - encodeURIComponent($('#ldap_serverconfig_chooser').val()); - //runs in the background, no callbacks necessary - return LdapWizard.ajax(param, LdapWizard.applyChanges, function(){}, 'detectEmailAttribute'); - }, - - detectGroupMemberAssoc: function() { - param = 'action=determineGroupMemberAssoc'+ - '&ldap_serverconfig_chooser='+ - encodeURIComponent($('#ldap_serverconfig_chooser').val()); - - return LdapWizard.ajax(param, - function(result) { - //pure background story - }, - function (result) { - // error handling - }, - 'determineGroupMemberAssoc' - ); - }, - - findAttributes: function() { - param = 'action=determineAttributes'+ - '&ldap_serverconfig_chooser='+ - encodeURIComponent($('#ldap_serverconfig_chooser').val()); - - LdapWizard.showSpinner('#ldap_loginfilter_attributes'); - LdapWizard.ajax(param, - function(result) { - $('#ldap_loginfilter_attributes').find('option').remove(); - for (var i in result.options['ldap_loginfilter_attributes']) { - //FIXME: move HTML into template - var attr = result.options['ldap_loginfilter_attributes'][i]; - $('#ldap_loginfilter_attributes').append( - "<option value='"+attr+"'>"+attr+"</option>"); - } - LdapWizard.hideSpinner('#ldap_loginfilter_attributes'); - LdapWizard.applyChanges(result); - $('#ldap_loginfilter_attributes').multiselect('refresh'); - if($('#rawLoginFilterContainer').hasClass('invisible')) { - $('#ldap_loginfilter_attributes').multiselect('enable'); - } - LdapWizard.postInitLoginFilter(); - }, - function (result) { - //deactivate if no attributes found - $('#ldap_loginfilter_attributes').multiselect( - {noneSelectedText : 'No attributes found'}); - $('#ldap_loginfilter_attributes').multiselect('disable'); - LdapWizard.hideSpinner('#ldap_loginfilter_attributes'); - }, - 'determineAttributes' - ); - }, - - findAvailableGroups: function(multisel, type) { - if(type !== 'Users' && type !== 'Groups') { - return false; - } - param = 'action=determineGroupsFor'+encodeURIComponent(type)+ - '&ldap_serverconfig_chooser='+ - encodeURIComponent($('#ldap_serverconfig_chooser').val()); - - LdapWizard.showSpinner('#'+multisel); - LdapWizard.ajax(param, - function(result) { - $('#'+multisel).find('option').remove(); - for (var i in result.options[multisel]) { - //FIXME: move HTML into template - objc = result.options[multisel][i]; - $('#'+multisel).append("<option value='"+objc+"'>"+objc+"</option>"); - } - LdapWizard.hideSpinner('#'+multisel); - LdapWizard.applyChanges(result); - $('#'+multisel).multiselect('refresh'); - part = type.slice(0, -1); - if($('#raw' + part + 'FilterContainer').hasClass('invisible')) { - //enable only when raw filter editing is not turned on - $('#'+multisel).multiselect('enable'); - } - if(type === 'Users') { - //required for initial save - filter = $('#ldap_userlist_filter').val(); - if(!filter) { - LdapWizard.saveMultiSelect(multisel, - $('#'+multisel).multiselect("getChecked")); - } - LdapWizard.userFilterAvailableGroupsHasRun = true; - LdapWizard.postInitUserFilter(); - } - }, - function (result) { - LdapWizard.hideSpinner('#'+multisel); - $('#'+multisel).multiselect('disable'); - if(type === 'Users') { - LdapWizard.userFilterAvailableGroupsHasRun = true; - LdapWizard.postInitUserFilter(); - } - }, - 'findAvailableGroupsFor' + type - ); - }, - - findObjectClasses: function(multisel, type) { - if(type !== 'User' && type !== 'Group') { - return false; - } - var param = 'action=determine'+encodeURIComponent(type)+'ObjectClasses'+ - '&ldap_serverconfig_chooser='+ - encodeURIComponent($('#ldap_serverconfig_chooser').val()); - - LdapWizard.showSpinner('#'+multisel); - LdapWizard.ajax(param, - function(result) { - $('#'+multisel).find('option').remove(); - for (var i in result.options[multisel]) { - //FIXME: move HTML into template - objc = result.options[multisel][i]; - $('#'+multisel).append("<option value='"+objc+"'>"+objc+"</option>"); - } - LdapWizard.hideSpinner('#'+multisel); - LdapWizard.applyChanges(result); - $('#'+multisel).multiselect('refresh'); - if(type === 'User') { - //required for initial save - filter = $('#ldap_userlist_filter').val(); - if(!filter) { - LdapWizard.saveMultiSelect(multisel, - $('#'+multisel).multiselect("getChecked")); - } - LdapWizard.userFilterObjectClassesHasRun = true; - LdapWizard.postInitUserFilter(); - } - }, - function (result) { - LdapWizard.hideSpinner('#'+multisel); - if(type === 'User') { - LdapWizard.userFilterObjectClassesHasRun = true; - LdapWizard.postInitUserFilter(); - } - //TODO: error handling - }, - 'determine' + type + 'ObjectClasses' - ); - }, - - functionalityCheck: function() { - //criteria to enable the connection: - // - host, port, basedn, user filter, login filter - var host = $('#ldap_host').val(); - var port = $('#ldap_port').val(); - var base = $('#ldap_base').val(); - var userfilter = $('#ldap_userlist_filter').val(); - var loginfilter = $('#ldap_login_filter').val(); - - //FIXME: activates a manually deactivated configuration. - if(host && port && base && userfilter && loginfilter) { - LdapWizard.updateStatusIndicator(true); - if($('#ldap_configuration_active').is(':checked')) { - return; - } - if(!LdapWizard.isConfigurationActiveControlLocked) { - //avoids a manually deactivated connection will be activated - //upon opening the admin page - $('#ldap_configuration_active').prop('checked', true); - LdapWizard.save($('#ldap_configuration_active')[0]); - } - } else { - if($('#ldap_configuration_active').is(':checked')) { - $('#ldap_configuration_active').prop('checked', false); - LdapWizard.save($('#ldap_configuration_active')[0]); - } - LdapWizard.updateStatusIndicator(false); - } - }, - - hideInfoBox: function() { - if(LdapWizard.checkInfoShown) { - $('#ldapWizard1 .ldapWizardInfo').addClass('invisible'); - LdapWizard.checkInfoShown = false; - } - }, - - hideSpinner: function(id) { - $(id+' + .wizSpinner').remove(); - $(id + " + button").css('display', 'inline'); - }, - - isConfigurationActiveControlLocked: true, - detectorsRunInXPMode: 0, - userDetectors: 1, - groupDetectors: 2, - - init: function() { - LdapWizard.detectorsRunInXPMode = 0; - LdapWizard.instantiateFilters(); - LdapWizard.admin.setExperienced($('#ldap_experienced_admin').is(':checked')); - LdapWizard.lastTestSuccessful = true; - LdapWizard.basicStatusCheck(); - LdapWizard.functionalityCheck(); - LdapWizard.isConfigurationActiveControlLocked = false; - }, - - initGroupFilter: function() { - LdapWizard.groupFilter.activate(); - }, - - /** init login filter tab section **/ - - initLoginFilter: function() { - LdapWizard.loginFilter.activate(); - }, - - postInitLoginFilter: function() { - if($('#rawLoginFilterContainer').hasClass('invisible')) { - LdapWizard.loginFilter.compose(); - } - }, - - /** end of init user filter tab section **/ - - initMultiSelect: function(object, id, caption) { - object.multiselect({ - header: false, - selectedList: 9, - noneSelectedText: caption, - click: function(event, ui) { - LdapWizard.saveMultiSelect(id, - $('#'+id).multiselect("getChecked")); - } - }); - }, - - hideTestSpinner:function (countMethod) { - var selector; - if(countMethod === 'countUsers') { - selector = '#rawUserFilterContainer .ldapGetEntryCount'; - } else { - selector = '#rawGroupFilterContainer .ldapGetEntryCount'; - } - LdapWizard.hideSpinner(selector); - }, - - /** init user filter tab section **/ - - instantiateFilters: function() { - delete LdapWizard.userFilter; - LdapWizard.userFilter = new LdapFilter('User', function(mode) { - if( !LdapWizard.admin.isExperienced() - || mode === LdapWizard.filterModeAssisted) { - LdapWizard.userFilter.updateCount(); - } - LdapWizard.userFilter.findFeatures(); - }); - $('#rawUserFilterContainer .ldapGetEntryCount').click(function(event) { - event.preventDefault(); - $('#ldap_user_count').text(''); - LdapWizard.showSpinner('#rawUserFilterContainer .ldapGetEntryCount'); - LdapWizard.userFilter.updateCount(LdapWizard.hideTestSpinner); - $('#ldap_user_count').removeClass('hidden'); - }); - - delete LdapWizard.loginFilter; - LdapWizard.loginFilter = new LdapFilter('Login', function(mode) { - LdapWizard.loginFilter.findFeatures(); - }); - - delete LdapWizard.groupFilter; - LdapWizard.groupFilter = new LdapFilter('Group', function(mode) { - if( !LdapWizard.admin.isExperienced() - || mode === LdapWizard.filterModeAssisted) { - LdapWizard.groupFilter.updateCount(); - } - LdapWizard.groupFilter.findFeatures(); - }); - $('#rawGroupFilterContainer .ldapGetEntryCount').click(function(event) { - event.preventDefault(); - $('#ldap_group_count').text(''); - LdapWizard.showSpinner('#rawGroupFilterContainer .ldapGetEntryCount'); - LdapWizard.groupFilter.updateCount(LdapWizard.hideTestSpinner); - $('#ldap_group_count').removeClass('hidden'); - }); - }, - - userFilterObjectClassesHasRun: false, - userFilterAvailableGroupsHasRun: false, - - initUserFilter: function() { - LdapWizard.userFilterObjectClassesHasRun = false; - LdapWizard.userFilterAvailableGroupsHasRun = false; - LdapWizard.userFilter.activate(); - }, - - postInitUserFilter: function() { - if(LdapWizard.userFilterObjectClassesHasRun && - LdapWizard.userFilterAvailableGroupsHasRun) { - LdapWizard.userFilter.compose(); - } - }, - - /** end of init user filter tab section **/ - - onTabChange: function(event, ui) { - if(LdapWizard.saveProcesses > 0) { - //do not allow to switch tabs as long as a save process is active - return false; - } - var newTabIndex = 0; - if(ui.newTab[0].id === '#ldapWizard2') { - LdapWizard.initUserFilter(); - newTabIndex = 1; - } else if(ui.newTab[0].id === '#ldapWizard3') { - LdapWizard.initLoginFilter(); - newTabIndex = 2; - } else if(ui.newTab[0].id === '#ldapWizard4') { - LdapWizard.initGroupFilter(); - newTabIndex = 3; - } - - var curTabIndex = $('#ldapSettings').tabs('option', 'active'); - if(curTabIndex >= 0 && curTabIndex <= 3) { - LdapWizard.controlUpdate(newTabIndex); - //run detectors in XP mode, when "Test Filter" button has not been - //clicked in order to make sure that email, displayname, member- - //group association attributes are properly set. - if( curTabIndex === 1 - && LdapWizard.admin.isExperienced - && !(LdapWizard.detecorsRunInXPMode & LdapWizard.userDetectors) - ) { - LdapWizard.runDetectors('User', function(){}); - } else if( curTabIndex === 3 - && LdapWizard.admin.isExperienced - && !(LdapWizard.detecorsRunInXPMode & LdapWizard.groupDetectors) - ) { - LdapWizard.runDetectors('Group', function(){}); - } - } - }, - - /** - * allows UserFilter, LoginFilter and GroupFilter to lookup objectClasses - * and similar again. This should be called after essential changes, e.g. - * Host or BaseDN changes, or positive functionality check - * - */ - allowFilterFeatureSearch: function () { - LdapWizard.userFilter.reAllowFeatureLookup(); - LdapWizard.loginFilter.reAllowFeatureLookup(); - LdapWizard.groupFilter.reAllowFeatureLookup(); - }, - - processChanges: function (triggerObj) { - LdapWizard.hideInfoBox(); - - if(triggerObj.id === 'ldap_host' - || triggerObj.id === 'ldap_port' - || triggerObj.id === 'ldap_dn' - || triggerObj.id === 'ldap_agent_password') { - LdapWizard.checkPort(); - if($('#ldap_port').val()) { - //if Port is already set, check BaseDN - LdapWizard.checkBaseDN(); - LdapWizard.allowFilterFeatureSearch(); - } - } - - if(triggerObj.id === 'ldap_loginfilter_username' - || triggerObj.id === 'ldap_loginfilter_email') { - LdapWizard.loginFilter.compose(); - } else if (!LdapWizard.admin.isExperienced()) { - if(triggerObj.id === 'ldap_userlist_filter') { - LdapWizard.userFilter.updateCount(); - } else if (triggerObj.id === 'ldap_group_filter') { - LdapWizard.groupFilter.updateCount(); - } - } - - if($('#ldapSettings').tabs('option', 'active') == 0) { - LdapWizard.basicStatusCheck(); - LdapWizard.functionalityCheck(); - } - }, - - save: function(inputObj) { - if(LdapWizard.blacklistRemove(inputObj.id)) { - return; - } - if($(inputObj).is('input[type=checkbox]') - && !$(inputObj).is(':checked')) { - val = 0; - } else { - val = $(inputObj).val(); - } - LdapWizard._save(inputObj, val); - }, - - /** - * updates user or group count on multiSelect close. Resets the event - * function subsequently. - * - * @param {LdapFilter} filter - * @param {Object} $multiSelectObj - */ - onMultiSelectClose: function(filter, $multiSelectObj) { - filter.updateCount(); - $multiSelectObj.multiselect({close: function(){}}); - }, - - saveMultiSelect: function(originalObj, resultObj) { - var values = ''; - for(var i = 0; i < resultObj.length; i++) { - values = values + "\n" + resultObj[i].value; - } - LdapWizard._save($('#'+originalObj)[0], $.trim(values)); - var $multiSelectObj = $('#'+originalObj); - var updateCount = !$multiSelectObj.multiselect("isOpen"); - var applyUpdateOnCloseToFilter; - if(originalObj === 'ldap_userfilter_objectclass' - || originalObj === 'ldap_userfilter_groups') { - LdapWizard.userFilter.compose(updateCount); - if(!updateCount) { - applyUpdateOnCloseToFilter = LdapWizard.userFilter; - } - //when user filter is changed afterwards, login filter needs to - //be adjusted, too - if(!LdapWizard.loginFilter) { - LdapWizard.initLoginFilter(); - } - LdapWizard.loginFilter.compose(); - } else if(originalObj === 'ldap_loginfilter_attributes') { - LdapWizard.loginFilter.compose(); - } else if(originalObj === 'ldap_groupfilter_objectclass' - || originalObj === 'ldap_groupfilter_groups') { - LdapWizard.groupFilter.compose(updateCount); - if(!updateCount) { - applyUpdateOnCloseToFilter = LdapWizard.groupFilter; - } - } - - if(applyUpdateOnCloseToFilter instanceof LdapFilter) { - $multiSelectObj.multiselect({ - close: function () { - LdapWizard.onMultiSelectClose( - applyUpdateOnCloseToFilter, $multiSelectObj); - } - }); - } - }, - - saveProcesses: 0, - _save: function(object, value) { - $('#ldap .ldap_saving').removeClass('hidden'); - LdapWizard.saveProcesses += 1; - $('#ldap *').addClass('save-cursor'); - param = 'cfgkey='+encodeURIComponent(object.id)+ - '&cfgval='+encodeURIComponent(value)+ - '&action=save'+ - '&ldap_serverconfig_chooser='+$('#ldap_serverconfig_chooser').val(); - - $.post( - OC.filePath('user_ldap','ajax','wizard.php'), - param, - function(result) { - LdapWizard.saveProcesses -= 1; - if(LdapWizard.saveProcesses === 0) { - $('#ldap .ldap_saving').addClass('hidden'); - $('#ldap *').removeClass('save-cursor'); - } - if(result.status === 'success') { - LdapWizard.processChanges(object); - } else { - console.log('Could not save value for ' + object.id); - } - } - ); - }, - - showInfoBox: function(text) { - $('#ldapWizard1 .ldapWizardInfo').text(text); - $('#ldapWizard1 .ldapWizardInfo').removeClass('invisible'); - LdapWizard.checkInfoShown = true; - }, - - showSpinner: function(id) { - if($(id + ' + .wizSpinner').length == 0) { - $(LdapWizard.spinner).insertAfter($(id)); - $(id + " + img + button").css('display', 'none'); - } - }, - - toggleRawFilter: function(container, moc, mg, stateVar, modeKey) { - var isUser = moc.indexOf('user') >= 0; - var filter = isUser ? LdapWizard.userFilter : LdapWizard.groupFilter; - //moc = multiselect objectclass - //mg = mutliselect groups - if($(container).hasClass('invisible')) { - filter.setMode(LdapWizard.filterModeRaw); - $(container).removeClass('invisible'); - $(moc).multiselect('disable'); - if($(mg).multiselect().attr('disabled') === 'disabled') { - LdapWizard[stateVar] = 'disable'; - } else { - LdapWizard[stateVar] = 'enable'; - } - $(mg).multiselect('disable'); - LdapWizard._save({ id: modeKey }, LdapWizard.filterModeRaw); - } else { - filter.setMode(LdapWizard.filterModeAssisted); - filter.findFeatures(); - $(container).addClass('invisible'); - $(mg).multiselect(LdapWizard[stateVar]); - $(moc).multiselect('enable'); - LdapWizard._save({ id: modeKey }, LdapWizard.filterModeAssisted); - if(isUser) { - LdapWizard.blacklistRemove('ldap_userlist_filter'); - LdapWizard.userFilter.compose(true); - } else { - LdapWizard.blacklistRemove('ldap_group_filter'); - LdapWizard.groupFilter.compose(true); - } - } - }, - - onToggleRawFilterConfirmation: function(currentMode, isRawVisible, callback) { - if( !LdapWizard.admin.isExperienced() - || currentMode === LdapWizard.filterModeAssisted - || (LdapWizard.admin.isExperienced() && !isRawVisible) - ) { - return callback(true); - } - - var confirmed = OCdialogs.confirm( - 'Switching the mode will enable automatic LDAP queries. Depending on your LDAP size they may take a while. Do you still want to switch the mode?', - 'Mode switch', - callback - ); - }, - - toggleRawGroupFilter: function() { - LdapWizard.onToggleRawFilterConfirmation( - LdapWizard.groupFilter.getMode(), - !$('#rawGroupFilterContainer').hasClass('invisible'), - function(confirmed) { - if(confirmed !== true) { - return; - } - - LdapWizard.blacklistRemove('ldap_group_filter'); - LdapWizard.toggleRawFilter('#rawGroupFilterContainer', - '#ldap_groupfilter_objectclass', - '#ldap_groupfilter_groups', - 'groupFilterGroupSelectState', - 'ldapGroupFilterMode' - ); - LdapWizard.admin.updateGroupTab(LdapWizard.groupFilter.getMode()); - } - ); - }, - - toggleRawLoginFilter: function() { - LdapWizard.onToggleRawFilterConfirmation( - LdapWizard.loginFilter.getMode(), - !$('#rawLoginFilterContainer').hasClass('invisible'), - function(confirmed) { - if(confirmed !== true) { - return; - } - - LdapWizard.blacklistRemove('ldap_login_filter'); - container = '#rawLoginFilterContainer'; - if($(container).hasClass('invisible')) { - $(container).removeClass('invisible'); - action = 'disable'; - property = 'disabled'; - mode = LdapWizard.filterModeRaw; - } else { - $(container).addClass('invisible'); - action = 'enable'; - property = false; - mode = LdapWizard.filterModeAssisted; - } - LdapWizard.loginFilter.setMode(mode); - LdapWizard.loginFilter.findFeatures(); - $('#ldap_loginfilter_attributes').multiselect(action); - $('#ldap_loginfilter_email').prop('disabled', property); - $('#ldap_loginfilter_username').prop('disabled', property); - LdapWizard._save({ id: 'ldapLoginFilterMode' }, mode); - if(action === 'enable') { - LdapWizard.loginFilter.compose(); - } - } - ); - }, - - toggleRawUserFilter: function() { - LdapWizard.onToggleRawFilterConfirmation( - LdapWizard.userFilter.getMode(), - !$('#rawUserFilterContainer').hasClass('invisible'), - function(confirmed) { - if(confirmed === true) { - LdapWizard.blacklistRemove('ldap_userlist_filter'); - LdapWizard.toggleRawFilter('#rawUserFilterContainer', - '#ldap_userfilter_objectclass', - '#ldap_userfilter_groups', - 'userFilterGroupSelectState', - 'ldapUserFilterMode' - ); - LdapWizard.admin.updateUserTab(LdapWizard.userFilter.getMode()); - } - } - ); - }, - - updateStatusIndicator: function(isComplete) { - if(isComplete) { - LdapConfiguration.testConfiguration( - //onSuccess - function(result) { - $('.ldap_config_state_indicator').text(t('user_ldap', - 'Configuration OK' - )); - $('.ldap_config_state_indicator').addClass('ldap_grey'); - $('.ldap_config_state_indicator_sign').removeClass('error'); - $('.ldap_config_state_indicator_sign').addClass('success'); - if(!LdapWizard.lastTestSuccessful) { - LdapWizard.lastTestSuccessful = true; - LdapWizard.allowFilterFeatureSearch(); - } - }, - //onError - function(result) { - $('.ldap_config_state_indicator').text(t('user_ldap', - 'Configuration incorrect' - )); - $('.ldap_config_state_indicator').removeClass('ldap_grey'); - $('.ldap_config_state_indicator_sign').addClass('error'); - $('.ldap_config_state_indicator_sign').removeClass('success'); - LdapWizard.lastTestSuccessful = false; - } - ); - } else { - $('.ldap_config_state_indicator').text(t('user_ldap', - 'Configuration incomplete' - )); - $('.ldap_config_state_indicator').removeClass('ldap_grey'); - $('.ldap_config_state_indicator_sign').removeClass('error'); - $('.ldap_config_state_indicator_sign').removeClass('success'); - } - } -}; - -$(document).ready(function() { - $('#ldapAdvancedAccordion').accordion({ heightStyle: 'content', animate: 'easeInOutCirc'}); - $('#ldapSettings').tabs({ beforeActivate: LdapWizard.onTabChange }); - $('.ldap_submit').button(); - $('.ldap_action_test_connection').button(); - $('#ldap_action_delete_configuration').button(); - LdapWizard.initMultiSelect($('#ldap_userfilter_groups'), - 'ldap_userfilter_groups', - t('user_ldap', 'Select groups')); - LdapWizard.initMultiSelect($('#ldap_userfilter_objectclass'), - 'ldap_userfilter_objectclass', - t('user_ldap', 'Select object classes')); - LdapWizard.initMultiSelect($('#ldap_loginfilter_attributes'), - 'ldap_loginfilter_attributes', - t('user_ldap', 'Select attributes')); - LdapWizard.initMultiSelect($('#ldap_groupfilter_groups'), - 'ldap_groupfilter_groups', - t('user_ldap', 'Select groups')); - LdapWizard.initMultiSelect($('#ldap_groupfilter_objectclass'), - 'ldap_groupfilter_objectclass', - t('user_ldap', 'Select object classes')); - - $('.lwautosave').change(function() { LdapWizard.save(this); }); - $('#toggleRawUserFilter').click(LdapWizard.toggleRawUserFilter); - $('#toggleRawGroupFilter').click(LdapWizard.toggleRawGroupFilter); - $('#toggleRawLoginFilter').click(LdapWizard.toggleRawLoginFilter); - LdapConfiguration.refreshConfig(); - $('.ldap_action_continue').click(function(event) { - event.preventDefault(); - LdapWizard.controlContinue(); - }); - $('.ldap_action_back').click(function(event) { - event.preventDefault(); - LdapWizard.controlBack(); - }); - $('.ldap_action_test_connection').click(function(event){ - event.preventDefault(); - LdapConfiguration.testConfiguration( - //onSuccess - function(result) { - OC.dialogs.alert( - result.message, - t('user_ldap', 'Connection test succeeded') - ); - }, - //onError - function(result) { - OC.dialogs.alert( - result.message, - t('user_ldap', 'Connection test failed') - ); - } - ); - }); - - $('#ldap_action_delete_configuration').click(function(event) { - event.preventDefault(); - OC.dialogs.confirm( - t('user_ldap', 'Do you really want to delete the current Server Configuration?'), - t('user_ldap', 'Confirm Deletion'), - function(deleteConfiguration) { - if(deleteConfiguration) { - LdapConfiguration.deleteConfiguration(); - } - } - ); - }); - - $('.ldap_submit').click(function(event) { - event.preventDefault(); - $.post( - OC.filePath('user_ldap','ajax','setConfiguration.php'), - $('#ldap').serialize(), - function (result) { - bgcolor = $('.ldap_submit').css('background'); - if (result.status === 'success') { - //the dealing with colors is a but ugly, but the jQuery version in use has issues with rgba colors - $('.ldap_submit').css('background', '#fff'); - $('.ldap_submit').effect('highlight', {'color':'#A8FA87'}, 5000, function() { - $('.ldap_submit').css('background', bgcolor); - }); - //update the Label in the config chooser - caption = $('#ldap_serverconfig_chooser option:selected:first').text(); - pretext = '. Server: '; - caption = caption.slice(0, caption.indexOf(pretext) + pretext.length); - caption = caption + $('#ldap_host').val(); - $('#ldap_serverconfig_chooser option:selected:first').text(caption); - - } else { - $('.ldap_submit').css('background', '#fff'); - $('.ldap_submit').effect('highlight', {'color':'#E97'}, 5000, function() { - $('.ldap_submit').css('background', bgcolor); - }); - } - } - ); - }); - - $('#ldap_action_clear_user_mappings').click(function(event) { - event.preventDefault(); - LdapConfiguration.clearMappings('user'); - }); - - $('#ldap_action_clear_group_mappings').click(function(event) { - event.preventDefault(); - LdapConfiguration.clearMappings('group'); - }); - - $('#ldap_serverconfig_chooser').change(function(event) { - value = $('#ldap_serverconfig_chooser option:selected:first').attr('value'); - if(value === 'NEW') { - LdapConfiguration.addConfiguration(false); - } else { - LdapConfiguration.refreshConfig(); - } - }); - - expAdminCB = $('#ldap_experienced_admin'); - LdapWizard.admin = new ExperiencedAdmin(LdapWizard, expAdminCB.is(':checked')); - expAdminCB.change(function() { - LdapWizard.admin.setExperienced($(this).is(':checked')); - }); -}); diff --git a/apps/user_ldap/js/wizard/configModel.js b/apps/user_ldap/js/wizard/configModel.js new file mode 100644 index 00000000000..c3f1e85b592 --- /dev/null +++ b/apps/user_ldap/js/wizard/configModel.js @@ -0,0 +1,606 @@ +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc this class represents a server configuration. It communicates + * with the ownCloud server to ensure to always have the up to date LDAP + * configuration. It sends various events that views can listen to and + * provides methods so they can modify the configuration based upon user + * input. This model is also extended by so-called "detectors" who let the + * ownCloud server try to auto-detect settings and manipulate the + * configuration as well. + * + * @constructor + */ + var ConfigModel = function() {}; + + ConfigModel.prototype = { + /** @constant {number} */ + FILTER_MODE_ASSISTED: 0, + /** @constant {number} */ + FILTER_MODE_RAW: 1, + + /** + * initializes the instance. Always call it after creating the instance. + * + * @param {OCA.LDAP.Wizard.WizardDetectorQueue} detectorQueue + */ + init: function (detectorQueue) { + /** @type {object} holds the configuration in key-value-pairs */ + this.configuration = {}; + /** @type {object} holds the subscribers that listen to the events */ + this.subscribers = {}; + /** @type {Array} holds registered detectors */ + this.detectors = []; + /** @type {boolean} whether a configuration is currently loading */ + this.loadingConfig = false; + + if(detectorQueue instanceof OCA.LDAP.Wizard.WizardDetectorQueue) { + /** @type {OCA.LDAP.Wizard.WizardDetectorQueue} */ + this.detectorQueue = detectorQueue; + } + }, + + /** + * loads a specified configuration + * + * @param {string} [configID] - the configuration id (or prefix) + */ + load: function (configID) { + if(this.loadingConfig) { + return; + } + this._resetDetectorQueue(); + + this.configID = configID; + var url = OC.generateUrl('apps/user_ldap/ajax/getConfiguration.php'); + var params = OC.buildQueryString({ldap_serverconfig_chooser: configID}); + this.loadingConfig = true; + var model = this; + $.post(url, params, function (result) { model._processLoadConfig(model, result) }); + }, + + /** + * creates a new LDAP configuration + * + * @param {boolean} [copyCurrent] - if true, the current configuration + * is copied, otherwise a blank one is created. + */ + newConfig: function(copyCurrent) { + this._resetDetectorQueue(); + + var url = OC.generateUrl('apps/user_ldap/ajax/getNewServerConfigPrefix.php'); + var params = {}; + if(copyCurrent === true) { + params['copyConfig'] = this.configID; + } + params = OC.buildQueryString(params); + var model = this; + copyCurrent = _.isUndefined(copyCurrent) ? false : copyCurrent; + $.post(url, params, function (result) { model._processNewConfigPrefix(model, result, copyCurrent) }); + }, + + /** + * deletes the current configuration. This method will not ask for + * confirmation, if desired it needs to be ensured by the caller. + * + * @param {string} [configID] - the configuration id (or prefix) + */ + deleteConfig: function(configID) { + var url = OC.generateUrl('apps/user_ldap/ajax/deleteConfiguration.php'); + var params = OC.buildQueryString({ldap_serverconfig_chooser: configID}); + var model = this; + $.post(url, params, function (result) { model._processDeleteConfig(model, result, configID) }); + }, + + /** + * @callback wizardCallBack + * @param {ConfigModel} [model] + * @param {OCA.LDAP.Wizard.WizardDetectorGeneric} [detector] + * @param {object} [result] - response from the ajax request + */ + + /** + * calls an AJAX endpoint at ownCloud. This method should be called by + * detectors only! + * + * @param {string} [params] - as return by OC.buildQueryString + * @param {wizardCallBack} [callback] + * @param {OCA.LDAP.Wizard.WizardDetectorGeneric} [detector] + * @returns {jqXHR} + */ + callWizard: function(params, callback, detector) { + return this.callAjax('wizard.php', params, callback, detector); + }, + + /** + * calls an AJAX endpoint at ownCloud. This method should be called by + * detectors only! + * + * @param {string} destination - the desired end point + * @param {string} [params] - as return by OC.buildQueryString + * @param {wizardCallBack} [callback] + * @param {OCA.LDAP.Wizard.WizardDetectorGeneric} [detector] + * @returns {jqXHR} + */ + callAjax: function(destination, params, callback, detector) { + var url = OC.generateUrl('apps/user_ldap/ajax/' + destination); + var model = this; + return $.post(url, params, function (result) { + callback(model, detector,result); + }); + }, + + /** + * setRequested Event + * + * @event ConfigModel#setRequested + * @type{object} - empty + */ + + /** + * modifies a configuration key. If a provided configuration key does + * not exist or the provided value equals the current setting, false is + * returned. Otherwise ownCloud server will be called to save the new + * value, an event will notify when this is done. True is returned when + * the request is sent, however it does not mean whether saving was + * successful or not. + * + * This method is supposed to be called by views, after the user did a + * change which needs to be saved. + * + * @param {string} [key] + * @param {string|number} [value] + * @returns {boolean} + * @fires {ConfigModel#setRequested} + */ + set: function(key, value) { + if(_.isUndefined(this.configuration[key])) { + console.warn('will not save undefined key: ' + key); + return false; + } + if(this.configuration[key] === value) { + return false; + } + this._broadcast('setRequested', {}); + var url = OC.generateUrl('apps/user_ldap/ajax/wizard.php'); + var objParams = { + ldap_serverconfig_chooser: this.configID, + action: 'save', + cfgkey: key, + cfgval: value + }; + var strParams = OC.buildQueryString(objParams); + var model = this; + $.post(url, strParams, function(result) { model._processSetResult(model, result, objParams) }); + return true; + }, + + /** + * configUpdated Event + * + * object property is a key-value-pair of the configuration key as index + * and its value. + * + * @event ConfigModel#configUpdated + * @type{object} + */ + + /** + * updates the model's configuration data. This should be called only, + * when a new configuration value was received from the ownCloud server. + * This is typically done by detectors, but never by views. + * + * Cancels with false if old and new values already match. + * + * @param {string} [key] + * @param {string} [value] + * @returns {boolean} + * @fires ConfigModel#configUpdated + */ + update: function(key, value) { + if(this.configuration[key] === value) { + return false; + } + if(!_.isUndefined(this.configuration[key])) { + // don't write e.g. count values to the configuration + // they don't go as feature, yet + this.configuration[key] = value; + } + var configPart = {}; + configPart[key] = value; + this._broadcast('configUpdated', configPart); + }, + + /** + * @typedef {object} FeaturePayload + * @property {string} feature + * @property {Array} data + */ + + /** + * informs about a detected LDAP "feature" (wider sense). For examples, + * the detected object classes for users or groups + * + * @param {FeaturePayload} payload + */ + inform: function(payload) { + this._broadcast('receivedLdapFeature', payload); + }, + + /** + * @typedef {object} ErrorPayload + * @property {string} message + * @property {string} relatedKey + */ + + /** + * broadcasts an error message, if a wizard reply ended up in an error. + * To be called by detectors. + * + * @param {ErrorPayload} payload + */ + gotServerError: function(payload) { + this._broadcast('serverError', payload); + }, + + /** + * detectionStarted Event + * + * @event ConfigModel#detectionStarted + * @type{string} - the target configuration key that is being + * auto-detected + */ + + /** + * lets the model broadcast the info that a detector starts to run + * + * supposed to be called by detectors only + * + * @param {string} [key] + * @fires ConfigModel#detectionStarted + */ + notifyAboutDetectionStart: function(key) { + this._broadcast('detectionStarted', key); + }, + + /** + * detectionCompleted Event + * + * @event ConfigModel#detectionCompleted + * @type{string} - the target configuration key that was + * auto-detected + */ + + /** + * lets the model broadcast the info that a detector run was completed + * + * supposed to be called by detectors only + * + * @param {string} [key] + * @fires ConfigModel#detectionCompleted + */ + notifyAboutDetectionCompletion: function(key) { + this._broadcast('detectionCompleted', key); + }, + + /** + * @callback listenerCallback + * @param {OCA.LDAP.Wizard.WizardTabGeneric|OCA.LDAP.Wizard.WizardView} [view] + * @param {object} [params] + */ + + /** + * registers a listener to an event + * + * the idea is that only views listen. + * + * @param {string} [name] - the event name + * @param {listenerCallback} [fn] + * @param {OCA.LDAP.Wizard.WizardTabGeneric|OCA.LDAP.Wizard.WizardView} [context] + */ + on: function(name, fn, context) { + if(_.isUndefined(this.subscribers[name])) { + this.subscribers[name] = []; + } + this.subscribers[name].push({fn: fn, context: context}); + }, + + /** + * starts a configuration test on the ownCloud server + */ + requestConfigurationTest: function() { + var url = OC.generateUrl('apps/user_ldap/ajax/testConfiguration.php'); + var params = OC.buildQueryString(this.configuration); + var model = this; + $.post(url, params, function(result) { model._processTestResult(model, result) }); + //TODO: make sure only one test is running at a time + }, + + /** + * the view may request a call to the wizard, for instance to fetch + * object classes or groups + * + * @param {string} featureKey + * @param {Object} [additionalParams] + */ + requestWizard: function(featureKey, additionalParams) { + var model = this; + var detectorCount = this.detectors.length; + var found = false; + for(var i = 0; i < detectorCount; i++) { + if(this.detectors[i].runsOnFeatureRequest(featureKey)) { + found = true; + (function (detector) { + model.detectorQueue.add(function() { + return detector.run(model, model.configID, additionalParams); + }); + })(model.detectors[i]); + } + } + if(!found) { + console.warn('No detector found for feature ' + featureKey); + } + }, + + /** + * resets the detector queue + * + * @private + */ + _resetDetectorQueue: function() { + if(!_.isUndefined(this.detectorQueue)) { + this.detectorQueue.reset(); + } + }, + + /** + * detectors can be registered herewith + * + * @param {OCA.LDAP.Wizard.WizardDetectorGeneric} [detector] + */ + registerDetector: function(detector) { + if(detector instanceof OCA.LDAP.Wizard.WizardDetectorGeneric) { + this.detectors.push(detector); + } + }, + + /** + * emits an event + * + * @param {string} [name] - the event name + * @param {*} [params] + * @private + */ + _broadcast: function(name, params) { + if(_.isUndefined(this.subscribers[name])) { + return; + } + var subscribers = this.subscribers[name]; + var subscriberCount = subscribers.length; + for(var i = 0; i < subscriberCount; i++) { + if(_.isUndefined(subscribers[i]['fn'])) { + console.warn('callback method is not defined. Event ' + name); + continue; + } + subscribers[i]['fn'](subscribers[i]['context'], params); + } + }, + + /** + * ConfigModel#configLoaded Event + * + * @event ConfigModel#configLoaded + * @type {object} - LDAP configuration as key-value-pairs + */ + + /** + * @typedef {object} ConfigLoadResponse + * @property {string} [status] + * @property {object} [configuration] - only present if status equals 'success' + */ + + /** + * processes the ajax response of a configuration load request + * + * @param {ConfigModel} [model] + * @param {ConfigLoadResponse} [result] + * @fires ConfigModel#configLoaded + * @private + */ + _processLoadConfig: function(model, result) { + model.configuration = {}; + if(result['status'] === 'success') { + $.each(result['configuration'], function(key, value) { + model.configuration[key] = value; + }); + } + model.loadingConfig = false; + model._broadcast('configLoaded', model.configuration); + }, + + /** + * @typedef {object} ConfigSetPayload + * @property {boolean} [isSuccess] + * @property {string} [key] + * @property {string} [value] + * @property {string} [errorMessage] + */ + + /** + * ConfigModel#setCompleted Event + * + * @event ConfigModel#setCompleted + * @type {ConfigSetPayload} + */ + + /** + * @typedef {object} ConfigSetResponse + * @property {string} [status] + * @property {object} [message] - might be present only in error cases + */ + + /** + * processes the ajax response of a configuration key set request + * + * @param {ConfigModel} [model] + * @param {ConfigSetResponse} [result] + * @param {object} [params] - the original changeSet + * @fires ConfigModel#configLoaded + * @private + */ + _processSetResult: function(model, result, params) { + var isSuccess = (result['status'] === 'success'); + if(isSuccess) { + model.configuration[params.cfgkey] = params.cfgval; + } + var payload = { + isSuccess: isSuccess, + key: params.cfgkey, + value: model.configuration[params.cfgkey], + errorMessage: _.isUndefined(result['message']) ? '' : result['message'] + }; + model._broadcast('setCompleted', payload); + + // let detectors run + // NOTE: detector's changes will not result in new _processSetResult + // calls, … in case they interfere it is because of this ;) + if(_.isUndefined(model.detectorQueue)) { + console.warn("DetectorQueue was not set, detectors will not be fired"); + return; + } + var detectorCount = model.detectors.length; + for(var i = 0; i < detectorCount; i++) { + if(model.detectors[i].triggersOn(params.cfgkey)) { + (function (detector) { + model.detectorQueue.add(function() { + return detector.run(model, model.configID); + }); + })(model.detectors[i]); + } + } + }, + + /** + * @typedef {object} ConfigTestPayload + * @property {boolean} [isSuccess] + */ + + /** + * ConfigModel#configurationTested Event + * + * @event ConfigModel#configurationTested + * @type {ConfigTestPayload} + */ + + /** + * @typedef {object} StatusResponse + * @property {string} [status] + */ + + /** + * processes the ajax response of a configuration test request + * + * @param {ConfigModel} [model] + * @param {StatusResponse} [result] + * @fires ConfigModel#configurationTested + * @private + */ + _processTestResult: function(model, result) { + var payload = { + isSuccess: (result['status'] === 'success') + }; + model._broadcast('configurationTested', payload); + }, + + /** + * @typedef {object} BasicConfigPayload + * @property {boolean} [isSuccess] + * @property {string} [configPrefix] - the new config ID + * @property {string} [errorMessage] + */ + + /** + * ConfigModel#newConfiguration Event + * + * @event ConfigModel#newConfiguration + * @type {BasicConfigPayload} + */ + + /** + * @typedef {object} NewConfigResponse + * @property {string} [status] + * @property {string} [configPrefix] + * @property {object} [defaults] - default configuration values + * @property {string} [message] - might only appear with status being + * not 'success' + */ + + /** + * processes the ajax response of a new configuration request + * + * @param {ConfigModel} [model] + * @param {NewConfigResponse} [result] + * @param {boolean} [copyCurrent] + * @fires ConfigModel#newConfiguration + * @fires ConfigModel#configLoaded + * @private + */ + _processNewConfigPrefix: function(model, result, copyCurrent) { + var isSuccess = (result['status'] === 'success'); + var payload = { + isSuccess: isSuccess, + configPrefix: result['configPrefix'], + errorMessage: _.isUndefined(result['message']) ? '' : result['message'] + }; + model._broadcast('newConfiguration', payload); + + if(isSuccess) { + this.configID = result['configPrefix']; + if(!copyCurrent) { + model.configuration = {}; + $.each(result['defaults'], function(key, value) { + model.configuration[key] = value; + }); + // view / tabs need to update with new blank config + model._broadcast('configLoaded', model.configuration); + } + } + }, + + /** + * ConfigModel#deleteConfiguration Event + * + * @event ConfigModel#deleteConfiguration + * @type {BasicConfigPayload} + */ + + /** + * processes the ajax response of a delete configuration request + * + * @param {ConfigModel} [model] + * @param {StatusResponse} [result] + * @param {string} [configID] + * @fires ConfigModel#deleteConfiguration + * @private + */ + _processDeleteConfig: function(model, result, configID) { + var isSuccess = (result['status'] === 'success'); + var payload = { + isSuccess: isSuccess, + configPrefix: configID, + errorMessage: _.isUndefined(result['message']) ? '' : result['message'] + }; + model._broadcast('deleteConfiguration', payload); + } + }; + + OCA.LDAP.Wizard.ConfigModel = ConfigModel; +})(); diff --git a/apps/user_ldap/js/wizard/controller.js b/apps/user_ldap/js/wizard/controller.js new file mode 100644 index 00000000000..7c1f0d5d818 --- /dev/null +++ b/apps/user_ldap/js/wizard/controller.js @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; +OCA.LDAP = {}; +OCA.LDAP.Wizard = {}; + +(function(){ + + /** + * @classdesc minimalistic controller that basically makes the view render + * + * @constructor + */ + var WizardController = function() {}; + + WizardController.prototype = { + /** + * initializes the instance. Always call it after creating the instance. + */ + init: function() { + this.view = false; + this.configModel = false; + }, + + /** + * sets the model instance + * + * @param {OCA.LDAP.Wizard.ConfigModel} [model] + */ + setModel: function(model) { + this.configModel = model; + }, + + /** + * sets the view instance + * + * @param {OCA.LDAP.Wizard.WizardView} [view] + */ + setView: function(view) { + this.view = view; + }, + + /** + * makes the view render i.e. ready to be used + */ + run: function() { + this.view.render(); + } + }; + + OCA.LDAP.Wizard.Controller = WizardController; +})(); diff --git a/apps/user_ldap/js/wizard/view.js b/apps/user_ldap/js/wizard/view.js new file mode 100644 index 00000000000..7743c277d61 --- /dev/null +++ b/apps/user_ldap/js/wizard/view.js @@ -0,0 +1,437 @@ +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc main view class. It takes care of tab-unrelated control + * elements (status bar, control buttons) and does or requests configuration + * checks. It also manages the separate tab views. + * + * @constructor + */ + var WizardView = function() {}; + + WizardView.prototype = { + /** @constant {number} */ + STATUS_ERROR: 0, + /** @constant {number} */ + STATUS_INCOMPLETE: 1, + /** @constant {number} */ + STATUS_SUCCESS: 2, + + /** + * initializes the instance. Always call it after creating the instance. + */ + init: function () { + this.tabs = {}; + this.tabs.server = new OCA.LDAP.Wizard.WizardTabElementary(); + this.$settings = $('#ldapSettings'); + this.$saveSpinners = $('.ldap_saving'); + this.saveProcesses = 0; + _.bindAll(this, 'onTabChange', 'onTestButtonClick'); + }, + + /** + * applies click events to the forward and backword buttons + */ + initControls: function() { + var view = this; + $('.ldap_action_continue').click(function(event) { + event.preventDefault(); + view._controlContinue(view); + }); + + $('.ldap_action_back').click(function(event) { + event.preventDefault(); + view._controlBack(view); + }); + + $('.ldap_action_test_connection').click(this.onTestButtonClick); + }, + + /** + * registers a tab + * + * @param {OCA.LDAP.Wizard.WizardTabGeneric} tabView + * @param {string} index + * @returns {boolean} + */ + registerTab: function(tabView, index) { + if( _.isUndefined(this.tabs[index]) + && tabView instanceof OCA.LDAP.Wizard.WizardTabGeneric + ) { + this.tabs[index] = tabView; + this.tabs[index].setModel(this.configModel); + return true; + } + return false; + }, + + /** + * checks certain config values for completeness and depending on them + * enables or disables non-elementary tabs. + */ + basicStatusCheck: function(view) { + var host = view.configModel.configuration.ldap_host; + var port = view.configModel.configuration.ldap_port; + var base = view.configModel.configuration.ldap_base; + var agent = view.configModel.configuration.ldap_dn; + var pwd = view.configModel.configuration.ldap_agent_password; + + if((host && port && base) && ((!agent && !pwd) || (agent && pwd))) { + view.enableTabs(); + } else { + view.disableTabs(); + } + }, + + /** + * if the configuration is sufficient the model is being request to + * perform a configuration test. Otherwise, the status indicator is + * being updated with the status "incomplete" + */ + functionalityCheck: function() { + // this method should be called only if necessary, because it may + // cause an LDAP request! + var host = this.configModel.configuration.ldap_host; + var port = this.configModel.configuration.ldap_port; + var base = this.configModel.configuration.ldap_base; + var userFilter = this.configModel.configuration.ldap_userlist_filter; + var loginFilter = this.configModel.configuration.ldap_login_filter; + + if(host && port && base && userFilter && loginFilter) { + this.configModel.requestConfigurationTest(); + } else { + this._updateStatusIndicator(this.STATUS_INCOMPLETE); + } + }, + + /** + * will request a functionality check if one of the related configuration + * settings was changed. + * + * @param {ConfigSetPayload|Object} [changeSet] + */ + considerFunctionalityCheck: function(changeSet) { + var testTriggers = [ + 'ldap_host', 'ldap_port', 'ldap_dn', 'ldap_agent_password', + 'ldap_base', 'ldap_userlist_filter', 'ldap_login_filter' + ]; + for(var key in changeSet) { + if($.inArray(key, testTriggers) >= 0) { + this.functionalityCheck(); + return; + } + } + }, + + /** + * keeps number of running save processes and shows a spinner if + * necessary + * + * @param {WizardView} [view] + * @listens ConfigModel#setRequested + */ + onSetRequested: function(view) { + view.saveProcesses += 1; + if(view.saveProcesses === 1) { + view.showSaveSpinner(); + } + }, + + /** + * keeps number of running save processes and hides the spinner if + * necessary. Also triggers checks, to adjust tabs state and status bar. + * + * @param {WizardView} [view] + * @param {ConfigSetPayload} [result] + * @listens ConfigModel#setCompleted + */ + onSetRequestDone: function(view, result) { + if(view.saveProcesses > 0) { + view.saveProcesses -= 1; + if(view.saveProcesses === 0) { + view.hideSaveSpinner(); + } + } + + view.basicStatusCheck(view); + var param = {}; + param[result.key] = 1; + view.considerFunctionalityCheck(param); + }, + + /** + * updates the status indicator based on the configuration test result + * + * @param {WizardView} [view] + * @param {ConfigTestPayload} [result] + * @listens ConfigModel#configurationTested + */ + onTestCompleted: function(view, result) { + if(result.isSuccess) { + view._updateStatusIndicator(view.STATUS_SUCCESS); + } else { + view._updateStatusIndicator(view.STATUS_ERROR); + } + }, + + /** + * triggers initial checks upon configuration loading to update status + * controls + * + * @param {WizardView} [view] + * @listens ConfigModel#configLoaded + */ + onConfigLoaded: function(view) { + view.basicStatusCheck(view); + view.functionalityCheck(); + }, + + /** + * reacts on attempts to switch to a different tab + * + * @param {object} event + * @param {object} ui + * @returns {boolean} + */ + onTabChange: function(event, ui) { + if(this.saveProcesses > 0) { + return false; + } + + var newTabID = ui.newTab[0].id; + if(newTabID === '#ldapWizard1') { + newTabID = 'server'; + } + var oldTabID = ui.oldTab[0].id; + if(oldTabID === '#ldapWizard1') { + oldTabID = 'server'; + } + if(!_.isUndefined(this.tabs[newTabID])) { + this.tabs[newTabID].isActive = true; + this.tabs[newTabID].onActivate(); + } else { + console.warn('Unreferenced activated tab ' + newTabID); + } + if(!_.isUndefined(this.tabs[oldTabID])) { + this.tabs[oldTabID].isActive = false; + } else { + console.warn('Unreferenced left tab ' + oldTabID); + } + + if(!_.isUndefined(this.tabs[newTabID])) { + this._controlUpdate(this.tabs[newTabID].tabIndex); + } + }, + + /** + * triggers checks upon configuration updates to keep status controls + * up to date + * + * @param {WizardView} [view] + * @param {object} [changeSet] + * @listens ConfigModel#configUpdated + */ + onConfigUpdated: function(view, changeSet) { + view.basicStatusCheck(view); + view.considerFunctionalityCheck(changeSet); + }, + + /** + * requests a configuration test + */ + onTestButtonClick: function() { + this.configModel.requestWizard('ldap_action_test_connection', this.configModel.configuration); + }, + + /** + * sets the model instance and registers event listeners + * + * @param {OCA.LDAP.Wizard.ConfigModel} [configModel] + */ + setModel: function(configModel) { + /** @type {OCA.LDAP.Wizard.ConfigModel} */ + this.configModel = configModel; + for(var i in this.tabs) { + this.tabs[i].setModel(configModel); + } + + // make sure this is definitely run after tabs did their work, order is important here + // for now this works, because tabs are supposed to register their listeners in their + // setModel() method. + // alternative: make Elementary Tab a Publisher as well. + this.configModel.on('configLoaded', this.onConfigLoaded, this); + this.configModel.on('configUpdated', this.onConfigUpdated, this); + this.configModel.on('setRequested', this.onSetRequested, this); + this.configModel.on('setCompleted', this.onSetRequestDone, this); + this.configModel.on('configurationTested', this.onTestCompleted, this); + }, + + /** + * enables tab and navigation buttons + */ + enableTabs: function() { + //do not use this function directly, use basicStatusCheck instead. + if(this.saveProcesses === 0) { + $('.ldap_action_continue').removeAttr('disabled'); + $('.ldap_action_back').removeAttr('disabled'); + this.$settings.tabs('option', 'disabled', []); + } + }, + + /** + * disables tab and navigation buttons + */ + disableTabs: function() { + $('.ldap_action_continue').attr('disabled', 'disabled'); + $('.ldap_action_back').attr('disabled', 'disabled'); + this.$settings.tabs('option', 'disabled', [1, 2, 3, 4, 5]); + }, + + /** + * shows a save spinner + */ + showSaveSpinner: function() { + this.$saveSpinners.removeClass('hidden'); + $('#ldap *').addClass('save-cursor'); + }, + + /** + * hides the save spinner + */ + hideSaveSpinner: function() { + this.$saveSpinners.addClass('hidden'); + $('#ldap *').removeClass('save-cursor'); + }, + + /** + * performs a config load request to the model + * + * @param {string} [configID] + * @private + */ + _requestConfig: function(configID) { + this.configModel.load(configID); + }, + + /** + * bootstraps the visual appearance and event listeners, as well as the + * first config + */ + render: function () { + $('#ldapAdvancedAccordion').accordion({ heightStyle: 'content', animate: 'easeInOutCirc'}); + this.$settings.tabs({}); + $('.ldap_submit').button(); + $('.ldap_action_test_connection').button(); + $('#ldapSettings').tabs({ beforeActivate: this.onTabChange }); + + this.initControls(); + this.disableTabs(); + + this._requestConfig(this.tabs.server.getConfigID()); + }, + + /** + * updates the status indicator / bar + * + * @param {number} [state] + * @private + */ + _updateStatusIndicator: function(state) { + var $indicator = $('.ldap_config_state_indicator'); + var $indicatorLight = $('.ldap_config_state_indicator_sign'); + + switch(state) { + case this.STATUS_ERROR: + $indicator.text(t('user_ldap', + 'Configuration incorrect' + )); + $indicator.removeClass('ldap_grey'); + $indicatorLight.addClass('error'); + $indicatorLight.removeClass('success'); + break; + case this.STATUS_INCOMPLETE: + $indicator.text(t('user_ldap', + 'Configuration incomplete' + )); + $indicator.removeClass('ldap_grey'); + $indicatorLight.removeClass('error'); + $indicatorLight.removeClass('success'); + break; + case this.STATUS_SUCCESS: + $indicator.text(t('user_ldap', 'Configuration OK')); + $indicator.addClass('ldap_grey'); + $indicatorLight.removeClass('error'); + $indicatorLight.addClass('success'); + if(!this.tabs.server.isActive) { + this.configModel.set('ldap_configuration_active', 1); + } + break; + } + }, + + /** + * handles a click on the Back button + * + * @param {WizardView} [view] + * @private + */ + _controlBack: function(view) { + var curTabIndex = view.$settings.tabs('option', 'active'); + if(curTabIndex == 0) { + return; + } + view.$settings.tabs('option', 'active', curTabIndex - 1); + view._controlUpdate(curTabIndex - 1); + }, + + /** + * handles a click on the Continue button + * + * @param {WizardView} [view] + * @private + */ + _controlContinue: function(view) { + var curTabIndex = view.$settings.tabs('option', 'active'); + if(curTabIndex == 3) { + return; + } + view.$settings.tabs('option', 'active', 1 + curTabIndex); + view._controlUpdate(curTabIndex + 1); + }, + + /** + * updates the controls (navigation buttons) + * + * @param {number} [nextTabIndex] - index of the tab being switched to + * @private + */ + _controlUpdate: function(nextTabIndex) { + if(nextTabIndex == 0) { + $('.ldap_action_back').addClass('invisible'); + $('.ldap_action_continue').removeClass('invisible'); + } else + if(nextTabIndex == 1) { + $('.ldap_action_back').removeClass('invisible'); + $('.ldap_action_continue').removeClass('invisible'); + } else + if(nextTabIndex == 2) { + $('.ldap_action_continue').removeClass('invisible'); + $('.ldap_action_back').removeClass('invisible'); + } else + if(nextTabIndex == 3) { + $('.ldap_action_back').removeClass('invisible'); + $('.ldap_action_continue').addClass('invisible'); + } + } + }; + + OCA.LDAP.Wizard.WizardView = WizardView; +})(); diff --git a/apps/user_ldap/js/wizard/wizard.js b/apps/user_ldap/js/wizard/wizard.js new file mode 100644 index 00000000000..e8450d1c78f --- /dev/null +++ b/apps/user_ldap/js/wizard/wizard.js @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + + + +/** + * initializes the wizard and related components and kicks it off. + */ + +(function() { + var Wizard = function() { + var detectorQueue = new OCA.LDAP.Wizard.WizardDetectorQueue(); + detectorQueue.init(); + + var detectors = []; + detectors.push(new OCA.LDAP.Wizard.WizardDetectorPort()); + detectors.push(new OCA.LDAP.Wizard.WizardDetectorBaseDN()); + detectors.push(new OCA.LDAP.Wizard.WizardDetectorEmailAttribute()); + detectors.push(new OCA.LDAP.Wizard.WizardDetectorUserDisplayNameAttribute()); + detectors.push(new OCA.LDAP.Wizard.WizardDetectorUserGroupAssociation()); + detectors.push(new OCA.LDAP.Wizard.WizardDetectorUserObjectClasses()); + detectors.push(new OCA.LDAP.Wizard.WizardDetectorGroupObjectClasses()); + detectors.push(new OCA.LDAP.Wizard.WizardDetectorGroupsForUsers()); + detectors.push(new OCA.LDAP.Wizard.WizardDetectorGroupsForGroups()); + detectors.push(new OCA.LDAP.Wizard.WizardDetectorFilterUser()); + detectors.push(new OCA.LDAP.Wizard.WizardDetectorFilterLogin()); + detectors.push(new OCA.LDAP.Wizard.WizardDetectorFilterGroup()); + detectors.push(new OCA.LDAP.Wizard.WizardDetectorUserCount()); + detectors.push(new OCA.LDAP.Wizard.WizardDetectorGroupCount()); + detectors.push(new OCA.LDAP.Wizard.WizardDetectorAvailableAttributes()); + detectors.push(new OCA.LDAP.Wizard.WizardDetectorTestLoginName()); + detectors.push(new OCA.LDAP.Wizard.WizardDetectorTestBaseDN()); + detectors.push(new OCA.LDAP.Wizard.WizardDetectorTestConfiguration()); + detectors.push(new OCA.LDAP.Wizard.WizardDetectorClearUserMappings()); + detectors.push(new OCA.LDAP.Wizard.WizardDetectorClearGroupMappings()); + + var model = new OCA.LDAP.Wizard.ConfigModel(); + model.init(detectorQueue); + // NOTE: order of detectors may play a role + // for example, BaseDN detector needs the port. The port is typically found + // by the Port Detector. If BaseDN detector was run first, it will not have + // all necessary information. Only after Port Detector was executed… + for (var i = 0; i <= detectors.length; i++) { + model.registerDetector(detectors[i]); + } + + var filterOnTypeFactory = new OCA.LDAP.Wizard.FilterOnTypeFactory(); + + var tabs = []; + tabs.push(new OCA.LDAP.Wizard.WizardTabUserFilter(filterOnTypeFactory, 1)); + tabs.push(new OCA.LDAP.Wizard.WizardTabLoginFilter(2)); + tabs.push(new OCA.LDAP.Wizard.WizardTabGroupFilter(filterOnTypeFactory, 3)); + tabs.push(new OCA.LDAP.Wizard.WizardTabAdvanced()); + tabs.push(new OCA.LDAP.Wizard.WizardTabExpert()); + + var view = new OCA.LDAP.Wizard.WizardView(model); + view.init(); + view.setModel(model); + for (var j = 0; j <= tabs.length; j++) { + view.registerTab(tabs[j], '#ldapWizard' + (j + 2)); + } + + var controller = new OCA.LDAP.Wizard.Controller(); + controller.init(); + controller.setView(view); + controller.setModel(model); + controller.run(); + } + + OCA.LDAP.Wizard.Wizard = Wizard; +})(); + +$(document).ready(function() { + new OCA.LDAP.Wizard.Wizard(); +}); diff --git a/apps/user_ldap/js/wizard/wizardDetectorAvailableAttributes.js b/apps/user_ldap/js/wizard/wizardDetectorAvailableAttributes.js new file mode 100644 index 00000000000..f0272351749 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorAvailableAttributes.js @@ -0,0 +1,59 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc an Attributes Detector. It executes the auto-detection of + * available attributes by the ownCloud server, if requirements are met. + * + * @constructor + */ + var WizardDetectorAvailableAttributes = OCA.LDAP.Wizard.WizardDetectorGeneric.subClass({ + /** @inheritdoc */ + init: function() { + // given, it is not a configuration key + this.setTargetKey('ldap_loginfilter_attributes'); + this.runsOnRequest = true; + }, + + /** + * runs the detector, if port is not set. + * + * @param {OCA.LDAP.Wizard.ConfigModel} model + * @param {string} configID - the configuration prefix + * @returns {boolean|jqXHR} + * @abstract + */ + run: function(model, configID) { + model.notifyAboutDetectionStart(this.getTargetKey()); + var params = OC.buildQueryString({ + action: 'determineAttributes', + ldap_serverconfig_chooser: configID + }); + return model.callWizard(params, this.processResult, this); + }, + + /** + * @inheritdoc + */ + processResult: function(model, detector, result) { + if(result.status === 'success') { + var payload = { + feature: 'AvailableAttributes', + data: result.options[detector.getTargetKey()] + }; + model.inform(payload); + } + this._super(model, detector, result); + } + }); + + OCA.LDAP.Wizard.WizardDetectorAvailableAttributes = WizardDetectorAvailableAttributes; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorBaseDN.js b/apps/user_ldap/js/wizard/wizardDetectorBaseDN.js new file mode 100644 index 00000000000..70b9923e58d --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorBaseDN.js @@ -0,0 +1,52 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc a Base DN Detector. It executes the auto-detection of the base + * DN by the ownCloud server, if requirements are met. + * + * @constructor + */ + var WizardDetectorBaseDN = OCA.LDAP.Wizard.WizardDetectorGeneric.subClass({ + /** @inheritdoc */ + init: function() { + this.setTargetKey('ldap_base'); + this.runsOnRequest = true; + }, + + /** + * runs the detector, if specified configuration settings are set and + * base DN is not set. + * + * @param {OCA.LDAP.Wizard.ConfigModel} model + * @param {string} configID - the configuration prefix + * @returns {boolean|jqXHR} + * @abstract + */ + run: function(model, configID) { + if( !model.configuration['ldap_host'] + || !model.configuration['ldap_port'] + + ) + { + return false; + } + model.notifyAboutDetectionStart(this.getTargetKey()); + var params = OC.buildQueryString({ + action: 'guessBaseDN', + ldap_serverconfig_chooser: configID + }); + return model.callWizard(params, this.processResult, this); + } + }); + + OCA.LDAP.Wizard.WizardDetectorBaseDN = WizardDetectorBaseDN; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorClearGroupMappings.js b/apps/user_ldap/js/wizard/wizardDetectorClearGroupMappings.js new file mode 100644 index 00000000000..c6ef0a9cab1 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorClearGroupMappings.js @@ -0,0 +1,30 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc requests clearing of user mappings + * + * @constructor + */ + var WizardDetectorClearGroupMappings = OCA.LDAP.Wizard.WizardDetectorTestAbstract.subClass({ + /** @inheritdoc */ + init: function() { + // given, it is not a configuration key + this.setTargetKey('ldap_action_clear_group_mappings'); + this.testName = 'ClearMappings'; + this.isLegacy = true; + this.legacyDestination = 'clearMappings.php'; + this.runsOnRequest = true; + } + }); + + OCA.LDAP.Wizard.WizardDetectorClearGroupMappings = WizardDetectorClearGroupMappings; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorClearUserMappings.js b/apps/user_ldap/js/wizard/wizardDetectorClearUserMappings.js new file mode 100644 index 00000000000..0e4811b39ea --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorClearUserMappings.js @@ -0,0 +1,30 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc requests clearing of user mappings + * + * @constructor + */ + var WizardDetectorClearUserMappings = OCA.LDAP.Wizard.WizardDetectorTestAbstract.subClass({ + /** @inheritdoc */ + init: function() { + // given, it is not a configuration key + this.setTargetKey('ldap_action_clear_user_mappings'); + this.testName = 'ClearMappings'; + this.isLegacy = true; + this.legacyDestination = 'clearMappings.php'; + this.runsOnRequest = true; + } + }); + + OCA.LDAP.Wizard.WizardDetectorClearUserMappings = WizardDetectorClearUserMappings; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorEmailAttribute.js b/apps/user_ldap/js/wizard/wizardDetectorEmailAttribute.js new file mode 100644 index 00000000000..5f177734681 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorEmailAttribute.js @@ -0,0 +1,38 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc let's the wizard backend count the available users + * + * @constructor + */ + var WizardDetectorEmailAttribute = OCA.LDAP.Wizard.WizardDetectorFilterSimpleRequestAbstract.subClass({ + init: function() { + this.setTargetKey('ldap_user_count'); + this.wizardMethod = 'detectEmailAttribute'; + this.runsOnRequest = true; + }, + + /** + * @inheritdoc + */ + run: function(model, configID) { + if(model.configuration.ldap_email_attr) { + // a value is already set. Don't overwrite and don't ask LDAP + // without reason. + return false; + } + this._super(model, configID); + } + }); + + OCA.LDAP.Wizard.WizardDetectorEmailAttribute = WizardDetectorEmailAttribute; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorFeatureAbstract.js b/apps/user_ldap/js/wizard/wizardDetectorFeatureAbstract.js new file mode 100644 index 00000000000..e025d8d6242 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorFeatureAbstract.js @@ -0,0 +1,52 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc abstract detector for detecting groups and object classes + * + * @constructor + */ + var WizardDetectorFeatureAbstract = OCA.LDAP.Wizard.WizardDetectorGeneric.subClass({ + /** + * runs the detector, if port is not set. + * + * @param {OCA.LDAP.Wizard.ConfigModel} model + * @param {string} configID - the configuration prefix + * @returns {boolean|jqXHR} + * @abstract + */ + run: function(model, configID) { + model.notifyAboutDetectionStart(this.getTargetKey()); + var params = OC.buildQueryString({ + action: this.wizardMethod, + ldap_serverconfig_chooser: configID + }); + return model.callWizard(params, this.processResult, this); + }, + + /** + * @inheritdoc + */ + processResult: function(model, detector, result) { + if(result.status === 'success') { + var payload = { + feature: detector.featureName, + data: result.options[detector.getTargetKey()] + }; + model.inform(payload); + } + + this._super(model, detector, result); + } + }); + + OCA.LDAP.Wizard.WizardDetectorFeatureAbstract = WizardDetectorFeatureAbstract; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorFilterGroup.js b/apps/user_ldap/js/wizard/wizardDetectorFilterGroup.js new file mode 100644 index 00000000000..cca889839e4 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorFilterGroup.js @@ -0,0 +1,31 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc a Port Detector. It executes the auto-detection of the port + * by the ownCloud server, if requirements are met. + * + * @constructor + */ + var WizardDetectorFilterGroup = OCA.LDAP.Wizard.WizardDetectorFilterSimpleRequestAbstract.subClass({ + init: function() { + this.setTrigger([ + 'ldap_groupfilter_groups', + 'ldap_groupfilter_objectclass' + ]); + this.setTargetKey('ldap_group_filter'); + + this.wizardMethod = 'getGroupFilter'; + } + }); + + OCA.LDAP.Wizard.WizardDetectorFilterGroup = WizardDetectorFilterGroup; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorFilterLogin.js b/apps/user_ldap/js/wizard/wizardDetectorFilterLogin.js new file mode 100644 index 00000000000..e796b81e0eb --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorFilterLogin.js @@ -0,0 +1,33 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc a Port Detector. It executes the auto-detection of the port + * by the ownCloud server, if requirements are met. + * + * @constructor + */ + var WizardDetectorFilterLogin = OCA.LDAP.Wizard.WizardDetectorFilterSimpleRequestAbstract.subClass({ + init: function() { + this.setTrigger([ + 'ldap_loginfilter_username', + 'ldap_loginfilter_email', + 'ldap_loginfilter_attributes' + ]); + this.setTargetKey('ldap_login_filter'); + this.runsOnRequest = true; + + this.wizardMethod = 'getUserLoginFilter'; + } + }); + + OCA.LDAP.Wizard.WizardDetectorFilterLogin = WizardDetectorFilterLogin; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorFilterUser.js b/apps/user_ldap/js/wizard/wizardDetectorFilterUser.js new file mode 100644 index 00000000000..d34e244a1f5 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorFilterUser.js @@ -0,0 +1,32 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc a Port Detector. It executes the auto-detection of the port + * by the ownCloud server, if requirements are met. + * + * @constructor + */ + var WizardDetectorFilterUser = OCA.LDAP.Wizard.WizardDetectorFilterSimpleRequestAbstract.subClass({ + init: function() { + this.setTrigger([ + 'ldap_userfilter_groups', + 'ldap_userfilter_objectclass' + ]); + this.setTargetKey('ldap_userlist_filter'); + this.runsOnRequest = true; + + this.wizardMethod = 'getUserListFilter'; + } + }); + + OCA.LDAP.Wizard.WizardDetectorFilterUser = WizardDetectorFilterUser; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorGeneric.js b/apps/user_ldap/js/wizard/wizardDetectorGeneric.js new file mode 100644 index 00000000000..fd80018943e --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorGeneric.js @@ -0,0 +1,117 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + /** + * @classdesc a generic (abstract) Detector template. A Detector's task is + * to kick off server side detection of certain LDAP features. It is invoked + * when changes to specified configuration keys happen. + * + * @constructor + */ + var WizardDetectorGeneric = OCA.LDAP.Wizard.WizardObject.subClass({ + /** + * initializes the instance. Always call it after creating the instance. + */ + init: function() { + this.setTrigger([]); + this.targetKey = ''; + this.runsOnRequest = false; + }, + + /** + * sets the configuration keys the detector is listening on + * + * @param {string[]} triggers + */ + setTrigger: function(triggers) { + this.triggers = triggers; + }, + + /** + * tests whether the detector is triggered by the provided key + * + * @param {string} key + * @returns {boolean} + */ + triggersOn: function(key) { + return ($.inArray(key, this.triggers) >= 0); + }, + + /** + * whether the detector runs on explicit request + * + * @param {string} key + * @returns {boolean} + */ + runsOnFeatureRequest: function(key) { + return !!(this.runsOnRequest && this.targetKey === key); + }, + + /** + * sets the configuration key the detector is attempting to auto-detect + * + * @param {string} key + */ + setTargetKey: function(key) { + this.targetKey = key; + }, + + /** + * returns the configuration key the detector is attempting to + * auto-detect + */ + getTargetKey: function() { + return this.targetKey; + }, + + /** + * runs the detector. This method is supposed to be implemented by the + * concrete detector. + * + * Must return false if the detector decides not to run. + * Must return a jqXHR object otherwise, which is provided by the + * model's callWizard() + * + * @param {OCA.LDAP.Wizard.ConfigModel} model + * @param {string} configID - the configuration prefix + * @returns {boolean|jqXHR} + * @abstract + */ + run: function(model, configID) { + // to be implemented by subClass + return false; + }, + + /** + * processes the result of the ownCloud server + * + * @param {OCA.LDAP.Wizard.ConfigModel} model + * @param {WizardDetectorGeneric} detector + * @param {object} result + */ + processResult: function(model, detector, result) { + model['notifyAboutDetectionCompletion'](detector.getTargetKey()); + if(result.status === 'success') { + for (var id in result.changes) { + // update and not set method, as values are already stored + model['update'](id, result.changes[id]); + } + } else { + var payload = { relatedKey: detector.targetKey }; + if(!_.isUndefined(result.message)) { + payload.message = result.message; + } + model.gotServerError(payload); + } + } + }); + + OCA.LDAP.Wizard.WizardDetectorGeneric = WizardDetectorGeneric; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorGroupCount.js b/apps/user_ldap/js/wizard/wizardDetectorGroupCount.js new file mode 100644 index 00000000000..12d7df7514b --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorGroupCount.js @@ -0,0 +1,27 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc a Port Detector. It executes the auto-detection of the port + * by the ownCloud server, if requirements are met. + * + * @constructor + */ + var WizardDetectorGroupCount = OCA.LDAP.Wizard.WizardDetectorFilterSimpleRequestAbstract.subClass({ + init: function() { + this.setTargetKey('ldap_group_count'); + this.wizardMethod = 'countGroups'; + this.runsOnRequest = true; + } + }); + + OCA.LDAP.Wizard.WizardDetectorGroupCount = WizardDetectorGroupCount; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorGroupObjectClasses.js b/apps/user_ldap/js/wizard/wizardDetectorGroupObjectClasses.js new file mode 100644 index 00000000000..6d6048b7986 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorGroupObjectClasses.js @@ -0,0 +1,29 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc discovers object classes for the groups tab + * + * @constructor + */ + var WizardDetectorGroupObjectClasses = OCA.LDAP.Wizard.WizardDetectorFeatureAbstract.subClass({ + /** @inheritdoc */ + init: function() { + // given, it is not a configuration key + this.setTargetKey('ldap_groupfilter_objectclass'); + this.wizardMethod = 'determineGroupObjectClasses'; + this.featureName = 'GroupObjectClasses'; + this.runsOnRequest = true; + } + }); + + OCA.LDAP.Wizard.WizardDetectorGroupObjectClasses = WizardDetectorGroupObjectClasses; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorGroupsForGroups.js b/apps/user_ldap/js/wizard/wizardDetectorGroupsForGroups.js new file mode 100644 index 00000000000..fbb3f02e10a --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorGroupsForGroups.js @@ -0,0 +1,29 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc detects groups for the groups tab + * + * @constructor + */ + var WizardDetectorGroupsForGroups = OCA.LDAP.Wizard.WizardDetectorFeatureAbstract.subClass({ + /** @inheritdoc */ + init: function() { + // given, it is not a configuration key + this.setTargetKey('ldap_groupfilter_groups'); + this.wizardMethod = 'determineGroupsForGroups'; + this.featureName = 'GroupsForGroups'; + this.runsOnRequest = true; + } + }); + + OCA.LDAP.Wizard.WizardDetectorGroupsForGroups = WizardDetectorGroupsForGroups; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorGroupsForUsers.js b/apps/user_ldap/js/wizard/wizardDetectorGroupsForUsers.js new file mode 100644 index 00000000000..fe67854c794 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorGroupsForUsers.js @@ -0,0 +1,29 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc detects groups for the users tab + * + * @constructor + */ + var WizardDetectorGroupsForUsers = OCA.LDAP.Wizard.WizardDetectorFeatureAbstract.subClass({ + /** @inheritdoc */ + init: function() { + // given, it is not a configuration key + this.setTargetKey('ldap_userfilter_groups'); + this.wizardMethod = 'determineGroupsForUsers'; + this.featureName = 'GroupsForUsers'; + this.runsOnRequest = true; + } + }); + + OCA.LDAP.Wizard.WizardDetectorGroupsForUsers = WizardDetectorGroupsForUsers; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorPort.js b/apps/user_ldap/js/wizard/wizardDetectorPort.js new file mode 100644 index 00000000000..ba075189667 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorPort.js @@ -0,0 +1,44 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc a Port Detector. It executes the auto-detection of the port + * by the ownCloud server, if requirements are met. + * + * @constructor + */ + var WizardDetectorPort = OCA.LDAP.Wizard.WizardDetectorGeneric.subClass({ + /** @inheritdoc */ + init: function() { + this.setTargetKey('ldap_port'); + this.runsOnRequest = true; + }, + + /** + * runs the detector, if port is not set. + * + * @param {OCA.LDAP.Wizard.ConfigModel} model + * @param {string} configID - the configuration prefix + * @returns {boolean|jqXHR} + * @abstract + */ + run: function(model, configID) { + model.notifyAboutDetectionStart('ldap_port'); + var params = OC.buildQueryString({ + action: 'guessPortAndTLS', + ldap_serverconfig_chooser: configID + }); + return model.callWizard(params, this.processResult, this); + } + }); + + OCA.LDAP.Wizard.WizardDetectorPort = WizardDetectorPort; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorQueue.js b/apps/user_ldap/js/wizard/wizardDetectorQueue.js new file mode 100644 index 00000000000..b6fa644558e --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorQueue.js @@ -0,0 +1,89 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + /** + * @classdesc only run detector is allowed to run at a time. Basically + * because we cannot have parallel LDAP connections per session. This + * queue is takes care of running all the detectors one after the other. + * + * @constructor + */ + var WizardDetectorQueue = OCA.LDAP.Wizard.WizardObject.subClass({ + /** + * initializes the instance. Always call it after creating the instance. + */ + init: function() { + this.queue = []; + this.isRunning = false; + }, + + /** + * empties the queue and cancels a possibly running request + */ + reset: function() { + this.queue = []; + if(!_.isUndefined(this.runningRequest)) { + this.runningRequest.abort(); + delete this.runningRequest; + } + this.isRunning = false; + }, + + /** + * a parameter-free callback that eventually executes the run method of + * the detector. + * + * @callback detectorCallBack + * @see OCA.LDAP.Wizard.ConfigModel._processSetResult + */ + + /** + * adds a detector to the queue and attempts to trigger to run the + * next job, because it might be the first. + * + * @param {detectorCallBack} callback + */ + add: function(callback) { + this.queue.push(callback); + this.next(); + }, + + /** + * Executes the next detector if none is running. This method is also + * automatically invoked after a detector finished. + */ + next: function() { + if(this.isRunning === true || this.queue.length === 0) { + return; + } + + this.isRunning = true; + var callback = this.queue.shift(); + var request = callback(); + + // we receive either false or a jqXHR object + // false in case the detector decided against executing + if(request === false) { + this.isRunning = false; + this.next(); + return; + } + this.runningRequest = request; + + var detectorQueue = this; + $.when(request).then(function() { + detectorQueue.isRunning = false; + detectorQueue.next(); + }); + } + }); + + OCA.LDAP.Wizard.WizardDetectorQueue = WizardDetectorQueue; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorSimpleRequestAbstract.js b/apps/user_ldap/js/wizard/wizardDetectorSimpleRequestAbstract.js new file mode 100644 index 00000000000..37e41f42a64 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorSimpleRequestAbstract.js @@ -0,0 +1,44 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc a Port Detector. It executes the auto-detection of the port + * by the ownCloud server, if requirements are met. + * + * @constructor + */ + var WizardDetectorFilterSimpleRequestAbstract = OCA.LDAP.Wizard.WizardDetectorGeneric.subClass({ + runsOnRequest: true, + + /** + * runs the detector, if port is not set. + * + * @param {OCA.LDAP.Wizard.ConfigModel} model + * @param {string} configID - the configuration prefix + * @returns {boolean|jqXHR} + * @abstract + */ + run: function(model, configID) { + if(_.isUndefined(this.wizardMethod)) { + console.warn('wizardMethod not set! ' + this.constructor); + return false; + } + model.notifyAboutDetectionStart(this.targetKey); + var params = OC.buildQueryString({ + action: this.wizardMethod, + ldap_serverconfig_chooser: configID + }); + return model.callWizard(params, this.processResult, this); + } + }); + + OCA.LDAP.Wizard.WizardDetectorFilterSimpleRequestAbstract = WizardDetectorFilterSimpleRequestAbstract; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorTestAbstract.js b/apps/user_ldap/js/wizard/wizardDetectorTestAbstract.js new file mode 100644 index 00000000000..df0b0a2200a --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorTestAbstract.js @@ -0,0 +1,63 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc a Port Detector. It executes the auto-detection of the port + * by the ownCloud server, if requirements are met. + * + * @constructor + */ + var WizardDetectorTestAbstract = OCA.LDAP.Wizard.WizardDetectorGeneric.subClass({ + isLegacy: false, + + /** + * runs the test + * + * @param {OCA.LDAP.Wizard.ConfigModel} model + * @param {string} configID - the configuration prefix + * @param {Object} params - additional parameters needed to send to the + * wizard + * @returns {boolean|jqXHR} + * @abstract + */ + run: function(model, configID, params) { + if(_.isUndefined(this.wizardMethod) && !this.isLegacy) { + console.warn('wizardMethod not set! ' + this.constructor); + return false; + } + model.notifyAboutDetectionStart(this.getTargetKey()); + params = params || {}; + params = OC.buildQueryString($.extend({ + action: this.wizardMethod, + ldap_serverconfig_chooser: configID + }, params)); + if(!this.isLegacy) { + return model.callWizard(params, this.processResult, this); + } else { + return model.callAjax(this.legacyDestination, params, this.processResult, this); + } + }, + + /** + * @inheritdoc + */ + processResult: function(model, detector, result) { + model['notifyAboutDetectionCompletion'](detector.getTargetKey()); + var payload = { + feature: detector.testName, + data: result + }; + model.inform(payload); + } + }); + + OCA.LDAP.Wizard.WizardDetectorTestAbstract = WizardDetectorTestAbstract; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorTestBaseDN.js b/apps/user_ldap/js/wizard/wizardDetectorTestBaseDN.js new file mode 100644 index 00000000000..52848819bd8 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorTestBaseDN.js @@ -0,0 +1,29 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc Tests, how many objects reside in the given base DN(s) + * + * @constructor + */ + var WizardDetectorTestBaseDN = OCA.LDAP.Wizard.WizardDetectorTestAbstract.subClass({ + /** @inheritdoc */ + init: function() { + // given, it is not a configuration key + this.setTargetKey('ldap_test_base'); + this.testName = 'TestBaseDN'; + this.wizardMethod = 'countInBaseDN'; + this.runsOnRequest = true; + } + }); + + OCA.LDAP.Wizard.WizardDetectorTestBaseDN = WizardDetectorTestBaseDN; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorTestConfiguration.js b/apps/user_ldap/js/wizard/wizardDetectorTestConfiguration.js new file mode 100644 index 00000000000..1308c182909 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorTestConfiguration.js @@ -0,0 +1,31 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc a Port Detector. It executes the auto-detection of the port + * by the ownCloud server, if requirements are met. + * + * @constructor + */ + var WizardDetectorTestConfiguration = OCA.LDAP.Wizard.WizardDetectorTestAbstract.subClass({ + /** @inheritdoc */ + init: function() { + // given, it is not a configuration key + this.setTargetKey('ldap_action_test_connection'); + this.testName = 'TestConfiguration'; + this.isLegacy = true; + this.legacyDestination = 'testConfiguration.php'; + this.runsOnRequest = true; + } + }); + + OCA.LDAP.Wizard.WizardDetectorTestConfiguration = WizardDetectorTestConfiguration; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorTestLoginName.js b/apps/user_ldap/js/wizard/wizardDetectorTestLoginName.js new file mode 100644 index 00000000000..260df5a0fe0 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorTestLoginName.js @@ -0,0 +1,30 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc checks whether the provided log in name can be resolved into + * a DN using the current login filter + * + * @constructor + */ + var WizardDetectorTestLoginName = OCA.LDAP.Wizard.WizardDetectorTestAbstract.subClass({ + /** @inheritdoc */ + init: function() { + // given, it is not a configuration key + this.setTargetKey('ldap_test_loginname'); + this.testName = 'TestLoginName'; + this.wizardMethod = 'testLoginName'; + this.runsOnRequest = true; + } + }); + + OCA.LDAP.Wizard.WizardDetectorTestLoginName = WizardDetectorTestLoginName; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorUserCount.js b/apps/user_ldap/js/wizard/wizardDetectorUserCount.js new file mode 100644 index 00000000000..bcff2cf3b10 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorUserCount.js @@ -0,0 +1,26 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc let's the wizard backend count the available users + * + * @constructor + */ + var WizardDetectorUserCount = OCA.LDAP.Wizard.WizardDetectorFilterSimpleRequestAbstract.subClass({ + init: function() { + this.setTargetKey('ldap_user_count'); + this.wizardMethod = 'countUsers'; + this.runsOnRequest = true; + } + }); + + OCA.LDAP.Wizard.WizardDetectorUserCount = WizardDetectorUserCount; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorUserDisplayNameAttribute.js b/apps/user_ldap/js/wizard/wizardDetectorUserDisplayNameAttribute.js new file mode 100644 index 00000000000..ae734480c1c --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorUserDisplayNameAttribute.js @@ -0,0 +1,39 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc let's the wizard backend count the available users + * + * @constructor + */ + var WizardDetectorUserDisplayNameAttribute = OCA.LDAP.Wizard.WizardDetectorFilterSimpleRequestAbstract.subClass({ + init: function() { + this.setTargetKey('ldap_user_count'); + this.wizardMethod = 'detectUserDisplayNameAttribute'; + this.runsOnRequest = true; + }, + + /** + * @inheritdoc + */ + run: function(model, configID) { + // default value has capital N. Detected values are always lowercase + if(model.configuration.ldap_display_name && model.configuration.ldap_display_name !== 'displayName') { + // a value is already set. Don't overwrite and don't ask LDAP + // without reason. + return false; + } + this._super(model, configID); + } + }); + + OCA.LDAP.Wizard.WizardDetectorUserDisplayNameAttribute = WizardDetectorUserDisplayNameAttribute; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorUserGroupAssociation.js b/apps/user_ldap/js/wizard/wizardDetectorUserGroupAssociation.js new file mode 100644 index 00000000000..953a0b909a6 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorUserGroupAssociation.js @@ -0,0 +1,40 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc let's the wizard backend count the available users + * + * @constructor + */ + var WizardDetectorUserGroupAssociation = OCA.LDAP.Wizard.WizardDetectorFilterSimpleRequestAbstract.subClass({ + init: function() { + this.setTargetKey('ldap_group_count'); + this.wizardMethod = 'determineGroupMemberAssoc'; + this.runsOnRequest = true; + }, + + /** + * @inheritdoc + */ + run: function(model, configID) { + // TODO: might be better with configuration marker as uniqueMember + // is a valid value (although probably less common then member and memberUid). + if(model.configuration.ldap_group_member_assoc_attribute && model.configuration.ldap_group_member_assoc_attribute !== 'uniqueMember') { + // a value is already set. Don't overwrite and don't ask LDAP + // without reason. + return false; + } + this._super(model, configID); + } + }); + + OCA.LDAP.Wizard.WizardDetectorUserGroupAssociation = WizardDetectorUserGroupAssociation; +})(); diff --git a/apps/user_ldap/js/wizard/wizardDetectorUserObjectClasses.js b/apps/user_ldap/js/wizard/wizardDetectorUserObjectClasses.js new file mode 100644 index 00000000000..0fa324a0809 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardDetectorUserObjectClasses.js @@ -0,0 +1,29 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc discovers object classes for the users tab + * + * @constructor + */ + var WizardDetectorUserObjectClasses = OCA.LDAP.Wizard.WizardDetectorFeatureAbstract.subClass({ + /** @inheritdoc */ + init: function() { + // given, it is not a configuration key + this.setTargetKey('ldap_userfilter_objectclass'); + this.wizardMethod = 'determineUserObjectClasses'; + this.featureName = 'UserObjectClasses'; + this.runsOnRequest = true; + } + }); + + OCA.LDAP.Wizard.WizardDetectorUserObjectClasses = WizardDetectorUserObjectClasses; +})(); diff --git a/apps/user_ldap/js/wizard/wizardFilterOnType.js b/apps/user_ldap/js/wizard/wizardFilterOnType.js new file mode 100644 index 00000000000..bb1871023a2 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardFilterOnType.js @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc filters a select box when a text element is typed in + */ + var FilterOnType = OCA.LDAP.Wizard.WizardObject.subClass({ + /** + * initializes a type filter on a text input for a select element + * + * @param {jQuery} $select + * @param {jQuery} $textInput + */ + init: function($select, $textInput) { + this.$select = $select; + this.$textInput = $textInput; + this.updateOptions(); + this.lastSearch = ''; + + var fity = this; + $textInput.bind('change keyup', function () { + if(fity.runID) { + window.clearTimeout(fity.runID); + } + fity.runID = window.setTimeout(function() { + fity.filter(fity); + }, 250); + }); + }, + + /** + * the options will be read in again. Should be called after a + * configuration switch. + */ + updateOptions: function() { + var options = []; + this.$select.find('option').each(function() { + options.push({ + value: $(this).val(), + normalized: $(this).val().toLowerCase() + } + ); + }); + this._options = options; + }, + + /** + * the actual search or filter method + * + * @param {FilterOnType} fity + */ + filter: function(fity) { + var filterVal = fity.$textInput.val().toLowerCase(); + if(filterVal === fity.lastSearch) { + return; + } + fity.lastSearch = filterVal; + fity.$select.empty(); + $.each(fity._options, function() { + if(!filterVal || this.normalized.indexOf(filterVal) > -1) { + fity.$select.append($('<option>').val(this.value).text(this.value)); + } + }); + delete(fity.runID); + } + }); + + OCA.LDAP.Wizard.FilterOnType = FilterOnType; +})(); diff --git a/apps/user_ldap/js/wizard/wizardFilterOnTypeFactory.js b/apps/user_ldap/js/wizard/wizardFilterOnTypeFactory.js new file mode 100644 index 00000000000..bd6511dc8b0 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardFilterOnTypeFactory.js @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc creates instances of OCA.LDAP.Wizard.FilterOnType upon request + */ + var FilterOnTypeFactory = OCA.LDAP.Wizard.WizardObject.subClass({ + /** + * initializes a type filter on a text input for a select element + * + * @param {jQuery} $select + * @param {jQuery} $textInput + */ + get: function($select, $textInput) { + return new OCA.LDAP.Wizard.FilterOnType($select, $textInput); + } + }); + + OCA.LDAP.Wizard.FilterOnTypeFactory = FilterOnTypeFactory; +})(); diff --git a/apps/user_ldap/js/wizard/wizardObject.js b/apps/user_ldap/js/wizard/wizardObject.js new file mode 100644 index 00000000000..a90f1533a26 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardObject.js @@ -0,0 +1,60 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + var initializing = false; + var superPattern = /xyz/.test(function() { xyz; }) ? /\b_super\b/ : /.*/; + + /** + * @classdesc a base class that allows inheritance + * + * @abstrcact + * @constructor + */ + var WizardObject = function(){}; + WizardObject.subClass = function(properties) { + var _super = this.prototype; + + initializing = true; + var proto = new this(); + initializing = false; + + for (var name in properties) { + proto[name] = + typeof properties[name] === "function" && + typeof _super[name] === 'function' && + superPattern.test(properties[name]) ? + (function (name, fn) { + return function () { + var tmp = this._super; + this._super = _super[name]; + var ret = fn.apply(this, arguments); + this._super = tmp; + return ret; + }; + })(name, properties[name]) : + properties[name]; + }; + + function Class() { + if(!initializing && this.init) { + this.init.apply(this, arguments); + } + } + + Class.prototype = proto; + Class.constructor = Class; + Class.subClass = arguments.callee; + return Class; + }; + + WizardObject.constructor = WizardObject; + + OCA.LDAP.Wizard.WizardObject = WizardObject; +})(); diff --git a/apps/user_ldap/js/wizard/wizardTabAbstractFilter.js b/apps/user_ldap/js/wizard/wizardTabAbstractFilter.js new file mode 100644 index 00000000000..024b6af65d0 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardTabAbstractFilter.js @@ -0,0 +1,378 @@ +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc This class represents the view belonging to the server tab + * in the LDAP wizard. + */ + var WizardTabAbstractFilter = OCA.LDAP.Wizard.WizardTabGeneric.subClass({ + /** + * @property {number} number that needs to exceeded to use complex group + * selection element + */ + _groupElementSwitchThreshold: 40, + + /** + * @property {boolean} - tells whether multiselect or complex element is + * used for selecting groups + */ + isComplexGroupChooser: false, + + /** @property {string} */ + tabID: '', + + /** + * initializes the instance. Always call it after initialization. + * concrete view must set managed items first, and then call the parent + * init. + * + * @param {OCA.LDAP.Wizard.FilterOnTypeFactory} fotf + * @param {number} [tabIndex] + * @param {string} [tabID] + */ + init: function (fotf, tabIndex, tabID) { + this._super(tabIndex, tabID); + + /** @type {OCA.LDAP.Wizard.FilterOnTypeFactory} */ + this.foTFactory = fotf; + this._initMultiSelect( + this.getGroupsItem().$element, + t('user_ldap', 'Select groups') + ); + this._initMultiSelect( + this.getObjectClassItem().$element, + t('user_ldap', 'Select object classes') + ); + this.filterName = this.getFilterItem().keyName; + this._initFilterModeSwitcher( + this.getToggleItem().$element, + this.getRawFilterContainerItem().$element, + [ this.getObjectClassItem().$element ], + this.getFilterModeKey(), + { + status: 'disabled', + $element: this.getGroupsItem().$element + } + ); + _.bindAll(this, 'onCountButtonClick', 'onSelectGroup', 'onDeselectGroup'); + this.getCountItem().$relatedElements.click(this.onCountButtonClick); + if(this.manyGroupsSupport) { + var $selectBtn = $(this.tabID).find('.ldapGroupListSelect'); + $selectBtn.click(this.onSelectGroup); + var $deselectBtn = $(this.tabID).find('.ldapGroupListDeselect'); + $deselectBtn.click(this.onDeselectGroup); + } + }, + + /** + * returns managed item for the object class chooser. must be + * implemented by concrete view + */ + getObjectClassItem: function () {}, + + /** + * returns managed item for the group chooser. must be + * implemented by concrete view + */ + getGroupsItem: function () {}, + + /** + * returns managed item for the effective filter. must be + * implemented by concrete view + */ + getFilterItem: function () {}, + + /** + * returns managed item for the toggle element. must be + * implemented by concrete view + */ + getToggleItem: function () {}, + + /** + * returns managed item for the raw filter container. must be + * implemented by concrete view + */ + getRawFilterContainerItem: function () {}, + + /** + * returns managed item for the count control. must be + * implemented by concrete view + */ + getCountItem: function () {}, + + /** + * returns name of the filter mode key. must be implemented by concrete + * view + */ + getFilterModeKey: function () {}, + + /** + * Sets the config model for this view and subscribes to some events. + * Also binds the config chooser to the model + * + * @param {OCA.LDAP.Wizard.ConfigModel} configModel + */ + setModel: function(configModel) { + this._super(configModel); + this.configModel.on('configLoaded', this.onConfigSwitch, this); + this.configModel.on('receivedLdapFeature', this.onFeatureReceived, this); + }, + + /** + * @inheritdoc + */ + _setFilterModeAssisted: function () { + this._super(); + if(this.isComplexGroupChooser) { + this.enableElement(this.getGroupsItem().$relatedElements); + } + }, + + /** + * @inheritdoc + */ + _setFilterModeRaw: function () { + this._super(); + if(this.manyGroupsSupport) { + this.disableElement(this.getGroupsItem().$relatedElements); + } + }, + + /** + * sets the selected user object classes + * + * @param {Array} classes + */ + setObjectClass: function(classes) { + this.setElementValue(this.getObjectClassItem().$element, classes); + this.getObjectClassItem().$element.multiselect('refresh'); + }, + + /** + * sets the selected groups + * + * @param {Array} groups + */ + setGroups: function(groups) { + if(!this.isComplexGroupChooser) { + this.setElementValue(this.getGroupsItem().$element, groups); + this.getGroupsItem().$element.multiselect('refresh'); + } else { + var $element = $(this.tabID).find('.ldapGroupListSelected'); + this.equipMultiSelect($element, groups); + } + }, + + /** + * sets the filter + * + * @param {string} filter + */ + setFilter: function(filter) { + this.setElementValue(this.getFilterItem().$element, filter); + this.$filterModeRawContainer.siblings('.ldapReadOnlyFilterContainer').find('.ldapFilterReadOnlyElement').text(filter); + }, + + /** + * sets the user count string + * + * @param {string} countInfo + */ + setCount: function(countInfo) { + this.setElementValue(this.getCountItem().$element, countInfo); + }, + + /** + * @inheritdoc + */ + considerFeatureRequests: function() { + if(!this.isActive) { + return; + } + if(this.getObjectClassItem().$element.find('option').length === 0) { + this.disableElement(this.getObjectClassItem().$element); + this.disableElement(this.getGroupsItem().$element); + if(this.parsedFilterMode === this.configModel.FILTER_MODE_ASSISTED) { + this.configModel.requestWizard(this.getObjectClassItem().keyName); + this.configModel.requestWizard(this.getGroupsItem().keyName); + } + } + }, + + /** + * updates (creates, if necessary) filterOnType instances + * + * @param {string} [only] - if only one search index should be updated + */ + updateFilterOnType: function(only) { + if(_.isUndefined(this.filterOnType)) { + this.filterOnType = []; + + var $availableGroups = $(this.tabID).find('.ldapGroupListAvailable'); + this.filterOnType.push(this.foTFactory.get( + $availableGroups, $(this.tabID).find('.ldapManyGroupsSearch') + )); + var $selectedGroups = $(this.tabID).find('.ldapGroupListSelected'); + this.filterOnType.push(this.foTFactory.get( + $selectedGroups, $(this.tabID).find('.ldapManyGroupsSearch') + )); + } else { + if(_.isUndefined || only.toLowerCase() === 'available') { + this.filterOnType[0].updateOptions(); + } + if(_.isUndefined || only.toLowerCase() === 'selected') { + this.filterOnType[1].updateOptions(); + } + } + }, + + /** + * @inheritdoc + */ + onActivate: function() { + this.considerFeatureRequests(); + }, + + /** + * resets the view when a configuration switch happened. + * + * @param {WizardTabAbstractFilter} view + * @param {Object} configuration + */ + onConfigSwitch: function(view, configuration) { + view.getObjectClassItem().$element.find('option').remove(); + view.getGroupsItem().$element.find('option').remove(); + view.getCountItem().$element.text(''); + $(view.tabID).find('.ldapGroupListAvailable').empty(); + $(view.tabID).find('.ldapGroupListSelected').empty(); + view.updateFilterOnType(); + $(view.tabID).find('.ldapManyGroupsSearch').val(''); + + if(view.isComplexGroupChooser) { + view.isComplexGroupChooser = false; + view.getGroupsItem().$element.multiselect({classes: view.multiSelectPluginClass}); + $(view.tabID).find(".ldapManyGroupsSupport").addClass('hidden'); + } + + view.onConfigLoaded(view, configuration); + }, + + /** + * @inheritdoc + */ + onConfigLoaded: function(view, configuration) { + for(var key in view.managedItems){ + if(!_.isUndefined(configuration[key])) { + var value = configuration[key]; + var methodName = view.managedItems[key].setMethod; + if(!_.isUndefined(view[methodName])) { + view[methodName](value); + // we reimplement it here to update the filter index + // for groups. Maybe we can isolate it? + if(methodName === 'setGroups') { + view.updateFilterOnType('selected'); + } + } + } + } + }, + + /** + * if UserObjectClasses are found, the corresponding element will be + * updated + * + * @param {WizardTabAbstractFilter} view + * @param {FeaturePayload} payload + */ + onFeatureReceived: function(view, payload) { + if(payload.feature === view.getObjectClassItem().featureName) { + view.equipMultiSelect(view.getObjectClassItem().$element, payload.data); + if( !view.getFilterItem().$element.val() + && view.parsedFilterMode === view.configModel.FILTER_MODE_ASSISTED + ) { + view.configModel.requestWizard(view.getFilterItem().keyName) + } + } else if (payload.feature === view.getGroupsItem().featureName) { + if(view.manyGroupsSupport && payload.data.length > view._groupElementSwitchThreshold) { + // we need to fill the left list box, excluding the values + // that are already selected + var $element = $(view.tabID).find('.ldapGroupListAvailable'); + var selected = view.configModel.configuration[view.getGroupsItem().keyName]; + var available = $(payload.data).not(selected).get(); + view.equipMultiSelect($element, available); + view.updateFilterOnType('available'); + $(view.tabID).find(".ldapManyGroupsSupport").removeClass('hidden'); + view.getGroupsItem().$element.multiselect({classes: view.multiSelectPluginClass + ' forceHidden'}); + view.isComplexGroupChooser = true; + } else { + view.isComplexGroupChooser = false; + view.equipMultiSelect(view.getGroupsItem().$element, payload.data); + view.getGroupsItem().$element.multiselect({classes: view.multiSelectPluginClass}); + $(view.tabID).find(".ldapManyGroupsSupport").addClass('hidden'); + + } + } + }, + + /** + * request to count the users with the current filter + * + * @param {Event} event + */ + onCountButtonClick: function(event) { + event.preventDefault(); + // let's clear the field + this.getCountItem().$element.text(''); + this.configModel.requestWizard(this.getCountItem().keyName); + }, + + /** + * saves groups when using the complex UI + * + * @param {Array} groups + * @returns {boolean} + * @private + */ + _saveGroups: function(groups) { + var toSave = ''; + $(groups).each(function() { toSave = toSave + "\n" + this; } ); + this.configModel.set(this.getGroupsItem().keyName, $.trim(toSave)); + }, + + /** + * acts on adding groups to the filter + */ + onSelectGroup: function() { + var $available = $(this.tabID).find('.ldapGroupListAvailable'); + var $selected = $(this.tabID).find('.ldapGroupListSelected'); + var selected = $.map($selected.find('option'), function(e) { return e.value; }); + + this._saveGroups(selected.concat($available.val())); + $available.find('option:selected').prependTo($selected); + this.updateFilterOnType(); + }, + + /** + * acts on removing groups to the filter + */ + onDeselectGroup: function() { + var $available = $(this.tabID).find('.ldapGroupListAvailable'); + var $selected = $(this.tabID).find('.ldapGroupListSelected'); + var selected = $.map($selected.find('option:not(:selected)'), function(e) { return e.value; }); + + this._saveGroups(selected); + $selected.find('option:selected').appendTo($available); + this.updateFilterOnType(); + } + + }); + + OCA.LDAP.Wizard.WizardTabAbstractFilter = WizardTabAbstractFilter; +})(); diff --git a/apps/user_ldap/js/wizard/wizardTabAdvanced.js b/apps/user_ldap/js/wizard/wizardTabAdvanced.js new file mode 100644 index 00000000000..a27ec87b7c4 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardTabAdvanced.js @@ -0,0 +1,330 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc This class represents the view belonging to the advanced tab + * in the LDAP wizard. + */ + var WizardTabAdvanced = OCA.LDAP.Wizard.WizardTabGeneric.subClass({ + /** + * initializes the instance. Always call it after initialization. + * + * @param tabIndex + * @param tabID + */ + init: function (tabIndex, tabID) { + this._super(tabIndex, tabID); + + var items = { + // Connection settings + ldap_configuration_active: { + $element: $('#ldap_configuration_active'), + setMethod: 'setConfigurationState' + }, + ldap_backup_host: { + $element: $('#ldap_backup_host'), + setMethod: 'setBackupHost' + }, + ldap_backup_port: { + $element: $('#ldap_backup_port'), + setMethod: 'setBackupPort' + }, + ldap_override_main_server: { + $element: $('#ldap_override_main_server'), + setMethod: 'setOverrideMainServerState' + }, + ldap_nocase: { + $element: $('#ldap_nocase'), + setMethod: 'setNoCase' + }, + ldap_turn_off_cert_check: { + $element: $('#ldap_turn_off_cert_check'), + setMethod: 'setCertCheckDisabled' + }, + ldap_cache_ttl: { + $element: $('#ldap_cache_ttl'), + setMethod: 'setCacheTTL' + }, + + //Directory Settings + ldap_display_name: { + $element: $('#ldap_display_name'), + setMethod: 'setUserDisplayName' + }, + ldap_base_users: { + $element: $('#ldap_base_users'), + setMethod: 'setBaseDNUsers' + }, + ldap_attributes_for_user_search: { + $element: $('#ldap_attributes_for_user_search'), + setMethod: 'setSearchAttributesUsers' + }, + ldap_group_display_name: { + $element: $('#ldap_group_display_name'), + setMethod: 'setGroupDisplayName' + }, + ldap_base_groups: { + $element: $('#ldap_base_groups'), + setMethod: 'setBaseDNGroups' + }, + ldap_attributes_for_group_search: { + $element: $('#ldap_attributes_for_group_search'), + setMethod: 'setSearchAttributesGroups' + }, + ldap_group_member_assoc_attribute: { + $element: $('#ldap_group_member_assoc_attribute'), + setMethod: 'setGroupMemberAssociationAttribute' + }, + ldap_nested_groups: { + $element: $('#ldap_nested_groups'), + setMethod: 'setUseNestedGroups' + }, + ldap_paging_size: { + $element: $('#ldap_paging_size'), + setMethod: 'setPagingSize' + }, + + //Special Attributes + ldap_quota_attr: { + $element: $('#ldap_quota_attr'), + setMethod: 'setQuotaAttribute' + }, + ldap_quota_def: { + $element: $('#ldap_quota_def'), + setMethod: 'setQuotaDefault' + }, + ldap_email_attr: { + $element: $('#ldap_email_attr'), + setMethod: 'setEmailAttribute' + }, + home_folder_naming_rule: { + $element: $('#home_folder_naming_rule'), + setMethod: 'setHomeFolderAttribute' + } + }; + this.setManagedItems(items); + }, + + /** + * Sets the config model for this view and subscribes to some events. + * Also binds the config chooser to the model + * + * @param {OCA.LDAP.Wizard.ConfigModel} configModel + */ + setModel: function(configModel) { + this._super(configModel); + this.configModel.on('configLoaded', this.onConfigLoaded, this); + this.configModel.on('receivedLdapFeature', this.onResultReceived, this); + }, + + /** + * updates the experienced admin check box + * + * @param {string} isConfigActive contains an int + */ + setConfigurationState: function(isConfigActive) { + this.setElementValue( + this.managedItems.ldap_configuration_active.$element, isConfigActive + ); + }, + + /** + * updates the backup host configuration text field + * + * @param {string} host + */ + setBackupHost: function(host) { + this.setElementValue(this.managedItems.ldap_backup_host.$element, host); + }, + + /** + * updates the backup port configuration text field + * + * @param {string} port + */ + setBackupPort: function(port) { + this.setElementValue(this.managedItems.ldap_backup_port.$element, port); + }, + + /** + * sets whether the main server should be overridden or not + * + * @param {string} doOverride contains an int + */ + setOverrideMainServerState: function(doOverride) { + this.setElementValue( + this.managedItems.ldap_override_main_server.$element, doOverride + ); + }, + + /** + * whether the server is case insensitive. This setting does not play + * a role anymore (probably never had). + * + * @param {string} noCase contains an int + */ + setNoCase: function(noCase) { + this.setElementValue(this.managedItems.ldap_nocase.$element, noCase); + }, + + /** + * sets whether the SSL/TLS certification check shout be disabled + * + * @param {string} doCertCheck contains an int + */ + setCertCheckDisabled: function(doCertCheck) { + this.setElementValue( + this.managedItems.ldap_turn_off_cert_check.$element, doCertCheck + ); + }, + + /** + * sets the time-to-live of the LDAP cache (in seconds) + * + * @param {string} cacheTTL contains an int + */ + setCacheTTL: function(cacheTTL) { + this.setElementValue(this.managedItems.ldap_cache_ttl.$element, cacheTTL); + }, + + /** + * sets the user display name attribute + * + * @param {string} attribute + */ + setUserDisplayName: function(attribute) { + this.setElementValue(this.managedItems.ldap_display_name.$element, attribute); + }, + + /** + * sets the Base DN for users + * + * @param {string} base + */ + setBaseDNUsers: function(base) { + this.setElementValue(this.managedItems.ldap_base_users.$element, base); + }, + + /** + * sets the attributes for user searches + * + * @param {string} attributes + */ + setSearchAttributesUsers: function(attributes) { + this.setElementValue(this.managedItems.ldap_attributes_for_user_search.$element, attributes); + }, + + /** + * sets the display name attribute for groups + * + * @param {string} attribute + */ + setGroupDisplayName: function(attribute) { + this.setElementValue(this.managedItems.ldap_group_display_name.$element, attribute); + }, + + /** + * sets the Base DN for groups + * + * @param {string} base + */ + setBaseDNGroups: function(base) { + this.setElementValue(this.managedItems.ldap_base_groups.$element, base); + }, + + /** + * sets the attributes for group search + * + * @param {string} attributes + */ + setSearchAttributesGroups: function(attributes) { + this.setElementValue(this.managedItems.ldap_attributes_for_group_search.$element, attributes); + }, + + /** + * sets the attribute for the association of users and groups + * + * @param {string} attribute + */ + setGroupMemberAssociationAttribute: function(attribute) { + this.setElementValue(this.managedItems.ldap_group_member_assoc_attribute.$element, attribute); + }, + + /** + * enabled or disables the use of nested groups (groups in groups in + * groups…) + * + * @param {string} useNestedGroups contains an int + */ + setUseNestedGroups: function(useNestedGroups) { + this.setElementValue(this.managedItems.ldap_nested_groups.$element, useNestedGroups); + }, + + /** + * sets the size of pages for paged search + * + * @param {string} size contains an int + */ + setPagingSize: function(size) { + this.setElementValue(this.managedItems.ldap_paging_size.$element, size); + }, + + /** + * sets the email attribute + * + * @param {string} attribute + */ + setEmailAttribute: function(attribute) { + this.setElementValue(this.managedItems.ldap_email_attr.$element, attribute); + }, + + /** + * sets the quota attribute + * + * @param {string} attribute + */ + setQuotaAttribute: function(attribute) { + this.setElementValue(this.managedItems.ldap_quota_attr.$element, attribute); + }, + + /** + * sets the default quota for LDAP users + * + * @param {string} quota contains an int + */ + setQuotaDefault: function(quota) { + this.setElementValue(this.managedItems.ldap_quota_def.$element, quota); + }, + + /** + * sets the attribute for the ownCloud user specific home folder location + * + * @param {string} attribute + */ + setHomeFolderAttribute: function(attribute) { + this.setElementValue(this.managedItems.home_folder_naming_rule.$element, attribute); + }, + + /** + * deals with the result of the Test Connection test + * + * @param {WizardTabAdvanced} view + * @param {FeaturePayload} payload + */ + onResultReceived: function(view, payload) { + if(payload.feature === 'TestConfiguration') { + OC.Notification.showTemporary(payload.data.message); + } + } + }); + + OCA.LDAP.Wizard.WizardTabAdvanced = WizardTabAdvanced; +})(); diff --git a/apps/user_ldap/js/wizard/wizardTabElementary.js b/apps/user_ldap/js/wizard/wizardTabElementary.js new file mode 100644 index 00000000000..c7767b9cf66 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardTabElementary.js @@ -0,0 +1,347 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc This class represents the view belonging to the server tab + * in the LDAP wizard. + */ + var WizardTabElementary = OCA.LDAP.Wizard.WizardTabGeneric.subClass({ + /** + * initializes the instance. Always call it after initialization. + * + * @param tabIndex + * @param tabID + */ + init: function (tabIndex, tabID) { + tabIndex = 0; + this._super(tabIndex, tabID); + this.isActive = true; + this.configChooserID = '#ldap_serverconfig_chooser'; + + var items = { + ldap_host: { + $element: $('#ldap_host'), + setMethod: 'setHost' + }, + ldap_port: { + $element: $('#ldap_port'), + setMethod: 'setPort', + $relatedElements: $('.ldapDetectPort') + }, + ldap_dn: { + $element: $('#ldap_dn'), + setMethod: 'setAgentDN' + }, + ldap_agent_password: { + $element: $('#ldap_agent_password'), + setMethod: 'setAgentPwd' + }, + ldap_base: { + $element: $('#ldap_base'), + setMethod: 'setBase', + $relatedElements: $('.ldapDetectBase, .ldapTestBase'), + $detectButton: $('.ldapDetectBase'), + $testButton: $('.ldapTestBase') + }, + ldap_base_test: { + $element: $('#ldap_base') + }, + ldap_experienced_admin: { + $element: $('#ldap_experienced_admin'), + setMethod: 'setExperiencedAdmin' + } + }; + this.setManagedItems(items); + _.bindAll(this, 'onPortButtonClick', 'onBaseDNButtonClick', 'onBaseDNTestButtonClick'); + this.managedItems.ldap_port.$relatedElements.click(this.onPortButtonClick); + this.managedItems.ldap_base.$detectButton.click(this.onBaseDNButtonClick); + this.managedItems.ldap_base.$testButton.click(this.onBaseDNTestButtonClick); + }, + + /** + * Sets the config model for this view and subscribes to some events. + * Also binds the config chooser to the model + * + * @param {OCA.LDAP.Wizard.ConfigModel} configModel + */ + setModel: function(configModel) { + this._super(configModel); + this.configModel.on('configLoaded', this.onConfigSwitch, this); + this.configModel.on('newConfiguration', this.onNewConfiguration, this); + this.configModel.on('deleteConfiguration', this.onDeleteConfiguration, this); + this.configModel.on('receivedLdapFeature', this.onTestResultReceived, this); + this._enableConfigChooser(); + this._enableConfigButtons(); + }, + + /** + * returns the currently selected configuration ID + * + * @returns {string} + */ + getConfigID: function() { + return $(this.configChooserID).val(); + }, + + /** + * updates the host configuration text field + * + * @param {string} host + */ + setHost: function(host) { + this.setElementValue(this.managedItems.ldap_host.$element, host); + if(host) { + this.enableElement(this.managedItems.ldap_port.$relatedElements); + } else { + this.disableElement(this.managedItems.ldap_port.$relatedElements); + } + }, + + /** + * updates the port configuration text field + * + * @param {string} port + */ + setPort: function(port) { + this.setElementValue(this.managedItems.ldap_port.$element, port); + }, + + /** + * updates the user (agent) DN text field + * + * @param {string} agentDN + */ + setAgentDN: function(agentDN) { + this.setElementValue(this.managedItems.ldap_dn.$element, agentDN); + }, + + /** + * updates the user (agent) password field + * + * @param {string} agentPwd + */ + setAgentPwd: function(agentPwd) { + this.setElementValue( + this.managedItems.ldap_agent_password.$element, agentPwd + ); + }, + /** + * updates the base DN text area + * + * @param {string} bases + */ + setBase: function(bases) { + this.setElementValue(this.managedItems.ldap_base.$element, bases); + if(!bases) { + this.disableElement(this.managedItems.ldap_base.$testButton); + } else { + this.enableElement(this.managedItems.ldap_base.$testButton); + } + }, + + /** + * updates the experienced admin check box + * + * @param {string} xpAdminMode contains an int + */ + setExperiencedAdmin: function(xpAdminMode) { + this.setElementValue( + this.managedItems.ldap_experienced_admin.$element, xpAdminMode + ); + }, + + /** + * @inheritdoc + */ + overrideErrorMessage: function(message, key) { + switch(key) { + case 'ldap_port': + if (message === 'Invalid credentials') { + return t('user_ldap', 'Please check the credentials, they seem to be wrong.'); + } else { + return t('user_ldap', 'Please specify the port, it could not be auto-detected.'); + } + break; + case 'ldap_base': + if( message === 'Server is unwilling to perform' + || message === 'Could not connect to LDAP' + ) { + return t('user_ldap', 'Base DN could not be auto-detected, please revise credentials, host and port.'); + } + return t('user_ldap', 'Could not detect Base DN, please enter it manually.'); + break; + } + return message; + }, + + /** + * resets the view when a configuration switch happened. + * + * @param {WizardTabElementary} view + * @param {Object} configuration + */ + onConfigSwitch: function(view, configuration) { + view.disableElement(view.managedItems.ldap_port.$relatedElements); + + view.onConfigLoaded(view, configuration); + }, + + /** + * updates the configuration chooser when a new configuration was added + * which also means it is being switched to. The configuration fields + * are updated on a different step. + * + * @param {WizardTabElementary} view + * @param {Object} result + */ + onNewConfiguration: function(view, result) { + if(result.isSuccess === true) { + $(view.configChooserID + ' option:selected').removeAttr('selected'); + var html = '<option value="'+result.configPrefix+'" selected="selected">'+t('user_ldap','{nthServer}. Server', {nthServer: $(view.configChooserID + ' option').length + 1})+'</option>'; + $(view.configChooserID + ' option:last').after(html); + } + }, + + /** + * updates the configuration chooser upon the deletion of a + * configuration and, if necessary, loads an existing one. + * + * @param view + * @param result + */ + onDeleteConfiguration: function(view, result) { + if(result.isSuccess === true) { + if(view.getConfigID() === result.configPrefix) { + // if the deleted value is still the selected one (99% of + // the cases), remove it from the list and load the topmost + $(view.configChooserID + ' option:selected').remove(); + $(view.configChooserID + ' option:first').select(); + if($(view.configChooserID + ' option').length < 2) { + view.configModel.newConfig(false); + } else { + view.configModel.load(view.getConfigID()); + } + } else { + // otherwise just remove the entry + $(view.configChooserID + ' option[value=' + result.configPrefix + ']').remove(); + } + } else { + OC.Notification.showTemporary(result.errorMessage); + } + }, + + /** + * Base DN test results will arrive here + * + * @param {WizardTabElementary} view + * @param {FeaturePayload} payload + */ + onTestResultReceived: function(view, payload) { + if(payload.feature === 'TestBaseDN') { + var message; + if(payload.data.status === 'success') { + var objectsFound = parseInt(payload.data.changes.ldap_test_base, 10); + if(objectsFound < 1) { + message = t('user_ldap', 'No object found in the given Base DN. Please revise.'); + } else if(objectsFound > 1000) { + message = t('user_ldap', 'More then 1.000 directory entries available.'); + } else { + message = t('user_ldap', objectsFound + ' entries available within the provided Base DN'); + } + } else { + message = t('user_ldap', 'An error occurred. Please check the Base DN, as well as connection settings and credentials.'); + if(payload.data.message) { + console.warn(payload.data.message); + } + } + OC.Notification.showTemporary(message); + } + }, + + /** + * request to count the users with the current filter + * + * @param {Event} event + */ + onPortButtonClick: function(event) { + event.preventDefault(); + this.configModel.requestWizard('ldap_port'); + }, + + /** + * request to count the users with the current filter + * + * @param {Event} event + */ + onBaseDNButtonClick: function(event) { + event.preventDefault(); + this.configModel.requestWizard('ldap_base'); + }, + + /** + * request to count the users with the current filter + * + * @param {Event} event + */ + onBaseDNTestButtonClick: function(event) { + event.preventDefault(); + this.configModel.requestWizard('ldap_test_base'); + }, + + /** + * registers the change event on the configuration chooser and makes + * the model load a newly selected configuration + * + * @private + */ + _enableConfigChooser: function() { + var view = this; + $(this.configChooserID).change(function(){ + var value = $(view.configChooserID + ' option:selected:first').attr('value'); + view.configModel.load(value); + }); + }, + + /** + * adds actions to the action buttons for configuration management + * + * @private + */ + _enableConfigButtons: function() { + var view = this; + $('#ldap_action_delete_configuration').click(function(event) { + event.preventDefault(); + OC.dialogs.confirm( + t('user_ldap', 'Do you really want to delete the current Server Configuration?'), + t('user_ldap', 'Confirm Deletion'), + function(doDelete) { + if(doDelete) { + view.configModel.deleteConfig(view.getConfigID()); + } + }, + false + ); + }); + + $('#ldap_action_add_configuration').click(function(event) { + event.preventDefault(); + view.configModel.newConfig(false); + }); + + $('#ldap_action_copy_configuration').click(function(event) { + event.preventDefault(); + view.configModel.newConfig(true); + }); + } + }); + + OCA.LDAP.Wizard.WizardTabElementary = WizardTabElementary; +})(); diff --git a/apps/user_ldap/js/wizard/wizardTabExpert.js b/apps/user_ldap/js/wizard/wizardTabExpert.js new file mode 100644 index 00000000000..7cfd49ba0f6 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardTabExpert.js @@ -0,0 +1,130 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc This class represents the view belonging to the expert tab + * in the LDAP wizard. + */ + var WizardTabExpert = OCA.LDAP.Wizard.WizardTabGeneric.subClass({ + /** + * initializes the instance. Always call it after initialization. + * + * @param tabIndex + * @param tabID + */ + init: function (tabIndex, tabID) { + this._super(tabIndex, tabID); + + var items = { + ldap_expert_username_attr: { + $element: $('#ldap_expert_username_attr'), + setMethod: 'setUsernameAttribute' + }, + ldap_expert_uuid_user_attr: { + $element: $('#ldap_expert_uuid_user_attr'), + setMethod: 'setUserUUIDAttribute' + }, + ldap_expert_uuid_group_attr: { + $element: $('#ldap_expert_uuid_group_attr'), + setMethod: 'setGroupUUIDAttribute' + }, + + //Buttons + ldap_action_clear_user_mappings: { + $element: $('#ldap_action_clear_user_mappings') + }, + ldap_action_clear_group_mappings: { + $element: $('#ldap_action_clear_group_mappings') + } + + }; + this.setManagedItems(items); + _.bindAll(this, 'onClearUserMappingsClick', 'onClearGroupMappingsClick'); + this.managedItems.ldap_action_clear_user_mappings.$element.click(this.onClearUserMappingsClick); + this.managedItems.ldap_action_clear_group_mappings.$element.click(this.onClearGroupMappingsClick); + }, + + /** + * Sets the config model for this view and subscribes to some events. + * Also binds the config chooser to the model + * + * @param {OCA.LDAP.Wizard.ConfigModel} configModel + */ + setModel: function(configModel) { + this._super(configModel); + this.configModel.on('configLoaded', this.onConfigLoaded, this); + this.configModel.on('receivedLdapFeature', this.onResultReceived, this); + }, + + /** + * sets the attribute to be used to create an ownCloud ID (username) + * + * @param {string} attribute + */ + setUsernameAttribute: function(attribute) { + this.setElementValue(this.managedItems.ldap_expert_username_attr.$element, attribute); + }, + + /** + * sets the attribute that provides an unique identifier per LDAP user + * entry + * + * @param {string} attribute + */ + setUserUUIDAttribute: function(attribute) { + this.setElementValue(this.managedItems.ldap_expert_uuid_user_attr.$element, attribute); + }, + + /** + * sets the attribute that provides an unique identifier per LDAP group + * entry + * + * @param {string} attribute + */ + setGroupUUIDAttribute: function(attribute) { + this.setElementValue(this.managedItems.ldap_expert_uuid_group_attr.$element, attribute); + }, + + /** + * requests clearing of all user mappings + */ + onClearUserMappingsClick: function() { + this.configModel.requestWizard('ldap_action_clear_user_mappings', {ldap_clear_mapping: 'user'}); + }, + + /** + * requests clearing of all group mappings + */ + onClearGroupMappingsClick: function() { + this.configModel.requestWizard('ldap_action_clear_group_mappings', {ldap_clear_mapping: 'group'}); + }, + + /** + * deals with the result of the Test Connection test + * + * @param {WizardTabAdvanced} view + * @param {FeaturePayload} payload + */ + onResultReceived: function(view, payload) { + if(payload.feature === 'ClearMappings') { + var message; + if(payload.data.status === 'success') { + message = t('user_ldap', 'Mappings cleared successfully!'); + } else { + message = t('user_ldap', 'Error while clearing the mappings.'); + } + OC.Notification.showTemporary(message); + } + } + }); + + OCA.LDAP.Wizard.WizardTabExpert = WizardTabExpert; +})(); diff --git a/apps/user_ldap/js/wizard/wizardTabGeneric.js b/apps/user_ldap/js/wizard/wizardTabGeneric.js new file mode 100644 index 00000000000..524d2a048a1 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardTabGeneric.js @@ -0,0 +1,547 @@ + +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc An abstract tab view + * @abstract + */ + var WizardTabGeneric = OCA.LDAP.Wizard.WizardObject.subClass({ + isActive: false, + + /** + * @property {string} - class that identifies a multiselect-plugin + * control. + */ + multiSelectPluginClass: 'multiSelectPlugin', + + /** @inheritdoc */ + init: function(tabIndex, tabID) { + this.tabIndex = tabIndex; + this.tabID = tabID; + this.spinner = $('.ldapSpinner').first().clone().removeClass('hidden'); + _.bindAll(this, '_toggleRawFilterMode', '_toggleRawFilterModeConfirmation'); + }, + + /** + * sets the configuration items that are managed by that view. + * + * The parameter contains key-value pairs the key being the + * configuration keys and the value being its setter method. + * + * @param {object} managedItems + */ + setManagedItems: function(managedItems) { + this.managedItems = managedItems; + this._enableAutoSave(); + }, + + /** + * Sets the config model. The concrete view likely wants to subscribe + * to events as well. + * + * @param {OCA.LDAP.Wizard.ConfigModel} configModel + */ + setModel: function(configModel) { + this.configModel = configModel; + this.parsedFilterMode = this.configModel.FILTER_MODE_ASSISTED; + this.configModel.on('detectionStarted', this.onDetectionStarted, this); + this.configModel.on('detectionCompleted', this.onDetectionCompleted, this); + this.configModel.on('serverError', this.onServerError, this); + this.configModel.on('setCompleted', this.onItemSaved, this); + this.configModel.on('configUpdated', this.onConfigLoaded, this); + }, + + /** + * the method can be used to display a different error/information + * message than provided by the ownCloud server response. The concrete + * Tab View may optionally implement it. Returning an empty string will + * avoid any notification. + * + * @param {string} message + * @param {string} key + * @returns {string} + */ + overrideErrorMessage: function(message, key) { + return message; + }, + + /** + * this is called by the main view, if the tab is being switched to. + * The concrete tab view can implement this if necessary. + */ + onActivate: function() { }, + + /** + * updates the tab when the model loaded a configuration and notified + * this view. + * + * @param {WizardTabGeneric} view - this instance + * @param {Object} configuration + */ + onConfigLoaded: function(view, configuration) { + for(var key in view.managedItems){ + if(!_.isUndefined(configuration[key])) { + var value = configuration[key]; + var methodName = view.managedItems[key].setMethod; + if(!_.isUndefined(view[methodName])) { + view[methodName](value); + } + } + } + }, + + /** + * reacts on a set action on the model and updates the tab with the + * valid value. + * + * @param {WizardTabGeneric} view + * @param {Object} result + */ + onItemSaved: function(view, result) { + if(!_.isUndefined(view.managedItems[result.key])) { + var methodName = view.managedItems[result.key].setMethod; + view[methodName](result.value); + if(!result.isSuccess) { + OC.Notification.showTemporary(t('user_ldap', 'Saving failed. Please make sure the database is in Operation. Reload before continuing.')); + console.warn(result.errorMessage); + } + } + }, + + /** + * displays server error messages. + * + * @param view + * @param payload + */ + onServerError: function(view, payload) { + if ( !_.isUndefined(view.managedItems[payload.relatedKey])) { + var message = view.overrideErrorMessage(payload.message, payload.relatedKey); + if(message) { + OC.Notification.showTemporary(message); + } + } + }, + + /** + * disables affected, managed fields if a detector is running against them + * + * @param {WizardTabGeneric} view + * @param {string} key + */ + onDetectionStarted: function(view, key) { + if(!_.isUndefined(view.managedItems[key])) { + view.disableElement(view.managedItems[key].$element); + if(!_.isUndefined(view.managedItems[key].$relatedElements)){ + view.disableElement(view.managedItems[key].$relatedElements); + } + view.attachSpinner(view.managedItems[key].$element.attr('id')); + } + }, + + /** + * enables affected, managed fields after a detector was run against them + * + * @param {WizardTabGeneric} view + * @param {string} key + */ + onDetectionCompleted: function(view, key) { + if(!_.isUndefined(view.managedItems[key])) { + view.enableElement(view.managedItems[key].$element); + if(!_.isUndefined(view.managedItems[key].$relatedElements)){ + view.enableElement(view.managedItems[key].$relatedElements); + } + view.removeSpinner(view.managedItems[key].$element.attr('id')); + } + }, + + /** + * sets the value to an HTML element. Checkboxes, text areas and (text) + * input fields are supported. + * + * @param {jQuery} $element - the target element + * @param {string|number|Array} value + */ + setElementValue: function($element, value) { + // deal with check box + if ($element.is('input[type=checkbox]')) { + this._setCheckBox($element, value); + return; + } + + // deal with text area + if ($element.is('textarea') && $.isArray(value)) { + value = value.join("\n"); + } + + if ($element.is('span')) { + $element.text(value); + } else { + $element.val(value); + } + }, + + /** + * replaces options on a multiselect element + * + * @param {jQuery} $element - the multiselect element + * @param {Array} options + */ + equipMultiSelect: function($element, options) { + $element.empty(); + for (var i in options) { + var name = options[i]; + $element.append($('<option>').val(name).text(name).attr('title', name)); + } + if(!$element.hasClass('ldapGroupList')) { + $element.multiselect('refresh'); + this.enableElement($element); + } + }, + + /** + * enables the specified HTML element + * + * @param {jQuery} $element + */ + enableElement: function($element) { + var isMS = $element.is('select[multiple]'); + var hasOptions = isMS ? ($element.find('option').length > 0) : false; + + if($element.hasClass(this.multiSelectPluginClass) && hasOptions) { + $element.multiselect("enable"); + } else if(!isMS || (isMS && hasOptions)) { + $element.prop('disabled', false); + } + }, + + /** + * disables the specified HTML element + * + * @param {jQuery} $element + */ + disableElement: function($element) { + if($element.hasClass(this.multiSelectPluginClass)) { + $element.multiselect("disable"); + } else { + $element.prop('disabled', 'disabled'); + } + }, + + /** + * attaches a spinner icon to the HTML element specified by ID + * + * @param {string} elementID + */ + attachSpinner: function(elementID) { + if($('#' + elementID + ' + .ldapSpinner').length == 0) { + var spinner = this.spinner.clone(); + var $element = $('#' + elementID); + $(spinner).insertAfter($element); + // and special treatment for multiselects: + if ($element.is('select[multiple]')) { + $('#' + elementID + " + img + button").css('display', 'none'); + } + } + }, + + /** + * removes the spinner icon from the HTML element specified by ID + * + * @param {string} elementID + */ + removeSpinner: function(elementID) { + $('#' + elementID+' + .ldapSpinner').remove(); + // and special treatment for multiselects: + $('#' + elementID + " + button").css('display', 'inline'); + }, + + /** + * whether the wizard works in experienced admin mode + * + * @returns {boolean} + */ + isExperiencedMode: function() { + return parseInt(this.configModel.configuration.ldap_experienced_admin, 10) === 1; + }, + + /** + * sets up auto-save functionality to the managed items + * + * @private + */ + _enableAutoSave: function() { + var view = this; + + for(var id in this.managedItems) { + if(_.isUndefined(this.managedItems[id].$element) + || _.isUndefined(this.managedItems[id].setMethod)) { + continue; + } + var $element = this.managedItems[id].$element; + if (!$element.is('select[multiple]')) { + $element.change(function() { + view._requestSave($(this)); + }); + } + } + }, + + /** + * initializes a multiSelect element + * + * @param {jQuery} $element + * @param {string} caption + * @private + */ + _initMultiSelect: function($element, caption) { + var view = this; + $element.multiselect({ + header: false, + selectedList: 9, + noneSelectedText: caption, + classes: this.multiSelectPluginClass, + close: function() { + view._requestSave($element); + } + }); + }, + + /** + * @typedef {object} viewSaveInfo + * @property {function} val + * @property {function} attr + * @property {function} is + */ + + /** + * requests a save operation from the model for a given value + * represented by a HTML element and its ID. + * + * @param {jQuery|viewSaveInfo} $element + * @private + */ + _requestSave: function($element) { + var value = ''; + if($element.is('input[type=checkbox]') + && !$element.is(':checked')) { + value = 0; + } else if ($element.is('select[multiple]')) { + var entries = $element.multiselect("getChecked"); + for(var i = 0; i < entries.length; i++) { + value = value + "\n" + entries[i].value; + } + value = $.trim(value); + } else { + value = $element.val(); + } + this.configModel.set($element.attr('id'), value); + }, + + /** + * updates a checkbox element according to the provided value + * + * @param {jQuery} $element + * @param {string|number} value + * @private + */ + _setCheckBox: function($element, value) { + if(parseInt(value, 10) === 1) { + $element.attr('checked', 'checked'); + } else { + $element.removeAttr('checked'); + } + }, + + /** + * this is called when the filter mode is switched to assisted. The + * concrete tab view should implement this, to load LDAP features + * (e.g. object classes, groups, attributes…), if necessary. + */ + considerFeatureRequests: function() {}, + + /** + * this is called when the filter mode is switched to Assisted. The + * concrete tab view should request the compilation of the respective + * filter. + */ + requestCompileFilter: function() { + this.configModel.requestWizard(this.filterName); + }, + + /** + * sets the filter mode according to the provided configuration value + * + * @param {string} mode + */ + setFilterMode: function(mode) { + if(parseInt(mode, 10) === this.configModel.FILTER_MODE_ASSISTED) { + this.parsedFilterMode = this.configModel.FILTER_MODE_ASSISTED; + this.considerFeatureRequests(); + this._setFilterModeAssisted(); + if(this.isActive) { + // filter compilation should happen only, if the mode was + // switched manually, but not when initiating the view + this.requestCompileFilter(); + } + } else { + this._setFilterModeRaw(); + this.parsedFilterMode = this.configModel.FILTER_MODE_RAW; + } + }, + + /** + * updates the UI so that it represents the assisted mode setting + * + * @private + */ + _setFilterModeAssisted: function() { + var view = this; + this.$filterModeRawContainer.addClass('invisible'); + var filter = this.$filterModeRawContainer.find('.ldapFilterInputElement').val(); + this.$filterModeRawContainer.siblings('.ldapReadOnlyFilterContainer').find('.ldapFilterReadOnlyElement').text(filter); + this.$filterModeRawContainer.siblings('.ldapReadOnlyFilterContainer').removeClass('hidden'); + $.each(this.filterModeDisableableElements, function(i, $element) { + view.enableElement($element); + }); + if(!_.isUndefined(this.filterModeStateElement)) { + if (this.filterModeStateElement.status === 'enabled') { + this.enableElement(this.filterModeStateElement.$element); + } else { + this.filterModeStateElement.status = 'disabled'; + } + } + }, + + /** + * updates the UI so that it represents the raw mode setting + * + * @private + */ + _setFilterModeRaw: function() { + var view = this; + this.$filterModeRawContainer.removeClass('invisible'); + this.$filterModeRawContainer.siblings('.ldapReadOnlyFilterContainer').addClass('hidden'); + $.each(this.filterModeDisableableElements, function (i, $element) { + view.disableElement($element); + }); + + if(!_.isUndefined(this.filterModeStateElement)) { + if(this.filterModeStateElement.$element.multiselect().attr('disabled') === 'disabled') { + this.filterModeStateElement.status = 'disabled'; + } else { + this.filterModeStateElement.status = 'enabled'; + } + } + if(!_.isUndefined(this.filterModeStateElement)) { + this.disableElement(this.filterModeStateElement.$element); + } + }, + + /** + * @callback toggleConfirmCallback + * @param {boolean} isConfirmed + */ + + /** + * shows a confirmation dialogue before switching from raw to assisted + * mode if experienced mode is enabled. + * + * @param {toggleConfirmCallback} toggleFnc + * @private + */ + _toggleRawFilterModeConfirmation: function(toggleFnc) { + if( !this.isExperiencedMode() + || this.parsedFilterMode === this.configModel.FILTER_MODE_ASSISTED + ) { + toggleFnc(true); + } else { + OCdialogs.confirm( + t('user_ldap', 'Switching the mode will enable automatic LDAP queries. Depending on your LDAP size they may take a while. Do you still want to switch the mode?'), + t('user_ldap', 'Mode switch'), + toggleFnc + ); + } + }, + + /** + * toggles the visibility of a raw filter container and so also the + * state of the multi-select controls. The model is requested to save + * the state. + */ + _toggleRawFilterMode: function() { + var view = this; + this._toggleRawFilterModeConfirmation(function(isConfirmed) { + if(!isConfirmed) { + return; + } + /** var {number} */ + var mode; + if (view.parsedFilterMode === view.configModel.FILTER_MODE_ASSISTED) { + mode = view.configModel.FILTER_MODE_RAW; + } else { + mode = view.configModel.FILTER_MODE_ASSISTED; + } + view.setFilterMode(mode); + /** @var {viewSaveInfo} */ + var saveInfo = { + val: function () { + return mode; + }, + attr: function () { + return view.filterModeKey; + }, + is: function () { + return false; + } + }; + view._requestSave(saveInfo); + }); + }, + + /** + * @typedef {object} filterModeStateElementObj + * @property {string} status - either "enabled" or "disabled" + * @property {jQuery} $element + */ + + /** + * initializes a raw filter mode switcher + * + * @param {jQuery} $switcher - the element receiving the click + * @param {jQuery} $filterModeRawContainer - contains the raw filter + * input elements + * @param {jQuery[]} filterModeDisableableElements - an array of elements + * not belonging to the raw filter part that shall be en/disabled. + * @param {string} filterModeKey - the setting key that save the state + * of the mode + * @param {filterModeStateElementObj} [filterModeStateElement] - one element + * which status (enabled or not) is tracked by a setting + * @private + */ + _initFilterModeSwitcher: function( + $switcher, + $filterModeRawContainer, + filterModeDisableableElements, + filterModeKey, + filterModeStateElement + ) { + this.$filterModeRawContainer = $filterModeRawContainer; + this.filterModeDisableableElements = filterModeDisableableElements; + this.filterModeStateElement = filterModeStateElement; + this.filterModeKey = filterModeKey; + $switcher.click(this._toggleRawFilterMode); + } + + }); + + OCA.LDAP.Wizard.WizardTabGeneric = WizardTabGeneric; +})(); diff --git a/apps/user_ldap/js/wizard/wizardTabGroupFilter.js b/apps/user_ldap/js/wizard/wizardTabGroupFilter.js new file mode 100644 index 00000000000..528b5d83670 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardTabGroupFilter.js @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc This class represents the view belonging to the server tab + * in the LDAP wizard. + */ + var WizardTabGroupFilter = OCA.LDAP.Wizard.WizardTabAbstractFilter.subClass({ + /** + * @inheritdoc + */ + init: function (fotf, tabIndex, tabID) { + tabID = '#ldapWizard4'; + var items = { + ldap_groupfilter_objectclass: { + $element: $('#ldap_groupfilter_objectclass'), + setMethod: 'setObjectClass', + keyName: 'ldap_groupfilter_objectclass', + featureName: 'GroupObjectClasses' + }, + ldap_group_filter_mode: { + setMethod: 'setFilterMode' + }, + ldap_groupfilter_groups: { + $element: $('#ldap_groupfilter_groups'), + setMethod: 'setGroups', + keyName: 'ldap_groupfilter_groups', + featureName: 'GroupsForGroups', + $relatedElements: $( + tabID + ' .ldapGroupListAvailable,' + + tabID + ' .ldapGroupListSelected,' + + tabID + ' .ldapManyGroupsSearch' + ) + }, + ldap_group_filter: { + $element: $('#ldap_group_filter'), + setMethod: 'setFilter', + keyName: 'ldap_group_filter' + }, + groupFilterRawToggle: { + $element: $('#toggleRawGroupFilter') + }, + groupFilterRawContainer: { + $element: $('#rawGroupFilterContainer') + }, + ldap_group_count: { + $element: $('#ldap_group_count'), + $relatedElements: $('.ldapGetGroupCount'), + setMethod: 'setCount', + keyName: 'ldap_group_count' + } + }; + this.setManagedItems(items); + this.manyGroupsSupport = true; + this._super(fotf, tabIndex, tabID); + }, + + /** + * @inheritdoc + * @returns {Object} + */ + getObjectClassItem: function () { + return this.managedItems.ldap_groupfilter_objectclass; + }, + + /** + * @inheritdoc + * @returns {Object} + */ + getGroupsItem: function () { + return this.managedItems.ldap_groupfilter_groups; + }, + + /** + * @inheritdoc + * @returns {Object} + */ + getFilterItem: function () { + return this.managedItems.ldap_group_filter; + }, + + /** + * @inheritdoc + * @returns {Object} + */ + getToggleItem: function () { + return this.managedItems.groupFilterRawToggle; + }, + + /** + * @inheritdoc + * @returns {Object} + */ + getRawFilterContainerItem: function () { + return this.managedItems.groupFilterRawContainer; + }, + + /** + * @inheritdoc + * @returns {Object} + */ + getCountItem: function () { + return this.managedItems.ldap_group_count; + }, + + /** + * @inheritdoc + * @returns {string} + */ + getFilterModeKey: function () { + return 'ldap_group_filter_mode'; + } + + }); + + OCA.LDAP.Wizard.WizardTabGroupFilter = WizardTabGroupFilter; +})(); diff --git a/apps/user_ldap/js/wizard/wizardTabLoginFilter.js b/apps/user_ldap/js/wizard/wizardTabLoginFilter.js new file mode 100644 index 00000000000..9438fd73346 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardTabLoginFilter.js @@ -0,0 +1,238 @@ +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc This class represents the view belonging to the login filter + * tab in the LDAP wizard. + */ + var WizardTabLoginFilter = OCA.LDAP.Wizard.WizardTabGeneric.subClass({ + /** + * initializes the instance. Always call it after initialization. + * + * @param tabIndex + * @param tabID + */ + init: function (tabIndex, tabID) { + this._super(tabIndex, tabID); + + var items = { + ldap_loginfilter_username: { + $element: $('#ldap_loginfilter_username'), + setMethod: 'setLoginAttributeUsername' + }, + ldap_loginfilter_email: { + $element: $('#ldap_loginfilter_email'), + setMethod: 'setLoginAttributeEmail' + }, + ldap_login_filter_mode: { + setMethod: 'setFilterMode' + }, + ldap_loginfilter_attributes: { + $element: $('#ldap_loginfilter_attributes'), + setMethod: 'setLoginAttributesOther' + }, + ldap_login_filter: { + $element: $('#ldap_login_filter'), + setMethod: 'setLoginFilter' + }, + loginFilterRawToggle: { + $element: $('#toggleRawLoginFilter') + }, + loginFilterRawContainer: { + $element: $('#rawLoginFilterContainer') + }, + ldap_test_loginname: { + $element: $('#ldap_test_loginname'), + $relatedElements: $('.ldapVerifyLoginName') + } + }; + this.setManagedItems(items); + + this.filterModeKey = 'ldapLoginFilterMode'; + this._initMultiSelect( + this.managedItems.ldap_loginfilter_attributes.$element, + t('user_ldap', 'Select attributes') + ); + this.filterName = 'ldap_login_filter'; + this._initFilterModeSwitcher( + this.managedItems.loginFilterRawToggle.$element, + this.managedItems.loginFilterRawContainer.$element, + [ + this.managedItems.ldap_loginfilter_username.$element, + this.managedItems.ldap_loginfilter_email.$element, + this.managedItems.ldap_loginfilter_attributes.$element + ], + 'ldap_login_filter_mode' + ); + _.bindAll(this, 'onVerifyClick'); + this.managedItems.ldap_test_loginname.$relatedElements.click(this.onVerifyClick); + }, + + /** + * Sets the config model for this view and subscribes to some events. + * Also binds the config chooser to the model + * + * @param {OCA.LDAP.Wizard.ConfigModel} configModel + */ + setModel: function(configModel) { + this._super(configModel); + this.configModel.on('configLoaded', this.onConfigSwitch, this); + this.configModel.on('receivedLdapFeature', this.onFeatureReceived, this); + }, + + /** + * sets the selected attributes + * + * @param {Array} attributes + */ + setLoginAttributesOther: function(attributes) { + this.setElementValue(this.managedItems.ldap_loginfilter_attributes.$element, attributes); + this.managedItems.ldap_loginfilter_attributes.$element.multiselect('refresh'); + }, + + /** + * sets the login list filter + * + * @param {string} filter + */ + setLoginFilter: function(filter) { + this.setElementValue(this.managedItems.ldap_login_filter.$element, filter); + this.$filterModeRawContainer.siblings('.ldapReadOnlyFilterContainer').find('.ldapFilterReadOnlyElement').text(filter); + }, + + /** + * updates the username attribute check box + * + * @param {string} useUsername contains an int + */ + setLoginAttributeUsername: function(useUsername) { + this.setElementValue( + this.managedItems.ldap_loginfilter_username.$element, useUsername + ); + }, + + /** + * updates the email attribute check box + * + * @param {string} useEmail contains an int + */ + setLoginAttributeEmail: function(useEmail) { + this.setElementValue( + this.managedItems.ldap_loginfilter_email.$element, useEmail + ); + }, + + /** + * presents the result of the login name test + * + * @param result + */ + handleLoginTestResult: function(result) { + var message; + var isHtml = false; + if(result.status === 'success') { + var usersFound = parseInt(result.changes.ldap_test_loginname, 10); + if(usersFound < 1) { + var filter = $('<p>').text(result.changes.ldap_test_effective_filter).html(); + message = t('user_ldap', 'User not found. Please check your login attributes and username. Effective filter (to copy-and-paste for command line validation): <br/>' + filter); + console.warn(filter); + isHtml = true; + } else if(usersFound === 1) { + message = t('user_ldap', 'User found and settings verified.'); + } else if(usersFound > 1) { + message = t('user_ldap', 'Settings verified, but one user found. Only the first will be able to login. Consider a more narrow filter.'); + } + } else { + message = t('user_ldap', 'An unspecified error occurred. Please check the settings and the log.'); + if(!_.isUndefined(result.message) && result.message) { + message = result.message; + } + if(message === 'Bad search filter') { + message = t('user_ldap', 'The search filter is invalid, probably due to syntax issues like uneven number of opened and closed brackets. Please revise.'); + } else if(message === 'connection error') { + message = t('user_ldap', 'A connection error to LDAP / AD occurred, please check host, port and credentials.'); + } else if(message === 'missing placeholder') { + message = t('user_ldap', 'The %uid placeholder is missing. It will be replaced with the login name when querying LDAP / AD.'); + } + } + OC.Notification.showTemporary(message, {isHTML: isHtml}); + }, + + /** + * @inheritdoc + */ + considerFeatureRequests: function() { + if(!this.isActive) { + return; + } + if(this.managedItems.ldap_loginfilter_attributes.$element.find('option').length === 0) { + this.disableElement(this.managedItems.ldap_loginfilter_attributes.$element); + if(this.parsedFilterMode === this.configModel.FILTER_MODE_ASSISTED) { + this.configModel.requestWizard('ldap_loginfilter_attributes'); + } + } + }, + + /** + * @inheritdoc + */ + onActivate: function() { + this.considerFeatureRequests(); + if(!this.managedItems.ldap_login_filter.$element.val()) { + this.configModel.requestWizard('ldap_login_filter'); + } + }, + + /** + * resets the view when a configuration switch happened. + * + * @param {WizardTabLoginFilter} view + * @param {Object} configuration + */ + onConfigSwitch: function(view, configuration) { + view.managedItems.ldap_loginfilter_attributes.$element.find('option').remove(); + + view.onConfigLoaded(view, configuration); + }, + + /** + * if UserObjectClasses are found, the corresponding element will be + * updated + * + * @param {WizardTabLoginFilter} view + * @param {FeaturePayload} payload + */ + onFeatureReceived: function(view, payload) { + if(payload.feature === 'AvailableAttributes') { + view.equipMultiSelect(view.managedItems.ldap_loginfilter_attributes.$element, payload.data); + } else if(payload.feature === 'TestLoginName') { + view.handleLoginTestResult(payload.data); + } + }, + + /** + * request to test the provided login name + * + * @param {Event} event + */ + onVerifyClick: function(event) { + event.preventDefault(); + var testLogin = this.managedItems.ldap_test_loginname.$element.val(); + if(!testLogin) { + OC.Notification.showTemporary(t('user_ldap', 'Please provide a login name to test against'), 3); + } else { + this.configModel.requestWizard('ldap_test_loginname', {ldap_test_loginname: testLogin}); + } + } + + }); + + OCA.LDAP.Wizard.WizardTabLoginFilter = WizardTabLoginFilter; +})(); diff --git a/apps/user_ldap/js/wizard/wizardTabUserFilter.js b/apps/user_ldap/js/wizard/wizardTabUserFilter.js new file mode 100644 index 00000000000..992c1ccf379 --- /dev/null +++ b/apps/user_ldap/js/wizard/wizardTabUserFilter.js @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +OCA = OCA || {}; + +(function() { + + /** + * @classdesc This class represents the view belonging to the server tab + * in the LDAP wizard. + */ + var WizardTabUserFilter = OCA.LDAP.Wizard.WizardTabAbstractFilter.subClass({ + /** + * @inheritdoc + */ + init: function (fotf, tabIndex, tabID) { + tabID = '#ldapWizard2'; + var items = { + ldap_userfilter_objectclass: { + $element: $('#ldap_userfilter_objectclass'), + setMethod: 'setObjectClass', + keyName: 'ldap_userfilter_objectclass', + featureName: 'UserObjectClasses' + }, + ldap_user_filter_mode: { + setMethod: 'setFilterMode' + }, + ldap_userfilter_groups: { + $element: $('#ldap_userfilter_groups'), + setMethod: 'setGroups', + keyName: 'ldap_userfilter_groups', + featureName: 'GroupsForUsers', + $relatedElements: $( + tabID + ' .ldapGroupListAvailable,' + + tabID + ' .ldapGroupListSelected,' + + tabID + ' .ldapManyGroupsSearch' + ) + }, + ldap_userlist_filter: { + $element: $('#ldap_userlist_filter'), + setMethod: 'setFilter', + keyName: 'ldap_userlist_filter' + }, + userFilterRawToggle: { + $element: $('#toggleRawUserFilter') + }, + userFilterRawContainer: { + $element: $('#rawUserFilterContainer') + }, + ldap_user_count: { + $element: $('#ldap_user_count'), + $relatedElements: $('.ldapGetUserCount'), + setMethod: 'setCount', + keyName: 'ldap_user_count' + } + }; + this.setManagedItems(items); + this.manyGroupsSupport = true; + this._super(fotf, tabIndex, tabID); + }, + + /** + * @inheritdoc + * @returns {Object} + */ + getObjectClassItem: function () { + return this.managedItems.ldap_userfilter_objectclass; + }, + + /** + * @inheritdoc + * @returns {Object} + */ + getGroupsItem: function () { + return this.managedItems.ldap_userfilter_groups; + }, + + /** + * @inheritdoc + * @returns {Object} + */ + getFilterItem: function () { + return this.managedItems.ldap_userlist_filter; + }, + + /** + * @inheritdoc + * @returns {Object} + */ + getToggleItem: function () { + return this.managedItems.userFilterRawToggle; + }, + + /** + * @inheritdoc + * @returns {Object} + */ + getRawFilterContainerItem: function () { + return this.managedItems.userFilterRawContainer; + }, + + /** + * @inheritdoc + * @returns {Object} + */ + getCountItem: function () { + return this.managedItems.ldap_user_count; + }, + + /** + * @inheritdoc + * @returns {string} + */ + getFilterModeKey: function () { + return 'ldap_user_filter_mode'; + }, + + /** + * @inheritdoc + */ + overrideErrorMessage: function(message, key) { + if( key === 'ldap_userfilter_groups' + && message === 'memberOf is not supported by the server' + ) { + message = t('user_ldap', 'The group box was disabled, because the LDAP / AD server does not support memberOf.'); + } + return message; + } + + }); + + OCA.LDAP.Wizard.WizardTabUserFilter = WizardTabUserFilter; +})(); diff --git a/apps/user_ldap/lib/access.php b/apps/user_ldap/lib/access.php index e7fb4165c36..f38d11d4be3 100644 --- a/apps/user_ldap/lib/access.php +++ b/apps/user_ldap/lib/access.php @@ -596,6 +596,22 @@ class Access extends LDAPUtility implements user\IUserTools { } /** + * fetches a list of users according to a provided loginName and utilizing + * the login filter. + * + * @param string $loginName + * @param array $attributes optional, list of attributes to read + * @return array + */ + public function fetchUsersByLoginName($loginName, $attributes = array('dn')) { + $loginName = $this->escapeFilterPart($loginName); + $filter = \OCP\Util::mb_str_replace( + '%uid', $loginName, $this->connection->ldapLoginFilter, 'UTF-8'); + $users = $this->fetchListOfUsers($filter, $attributes); + return $users; + } + + /** * @param string $filter * @param string|string[] $attr * @param int $limit @@ -687,6 +703,17 @@ class Access extends LDAPUtility implements user\IUserTools { } /** + * returns the number of available objects on the base DN + * + * @param int|null $limit + * @param int|null $offset + * @return int|bool + */ + public function countObjects($limit = null, $offset = null) { + return $this->count('objectclass=*', $this->connection->ldapBase, array('dn'), $limit, $offset); + } + + /** * retrieved. Results will according to the order in the array. * @param int $limit optional, maximum results to be counted * @param int $offset optional, a starting point diff --git a/apps/user_ldap/lib/configuration.php b/apps/user_ldap/lib/configuration.php index 9b01fd2e559..373c5b48417 100644 --- a/apps/user_ldap/lib/configuration.php +++ b/apps/user_ldap/lib/configuration.php @@ -201,11 +201,14 @@ class Configuration { case 'ldapAgentPassword': $readMethod = 'getPwd'; break; - case 'ldapUserDisplayName': case 'ldapGroupDisplayName': $readMethod = 'getLcValue'; break; + case 'ldapUserDisplayName': default: + // user display name does not lower case because + // we rely on an upper case N as indicator whether to + // auto-detect it or not. FIXME $readMethod = 'getValue'; break; } @@ -374,7 +377,7 @@ class Configuration { 'ldap_groupfilter_groups' => '', 'ldap_display_name' => 'displayName', 'ldap_group_display_name' => 'cn', - 'ldap_tls' => 1, + 'ldap_tls' => 0, 'ldap_nocase' => 0, 'ldap_quota_def' => '', 'ldap_quota_attr' => '', diff --git a/apps/user_ldap/lib/wizard.php b/apps/user_ldap/lib/wizard.php index 97f3002ccaf..7bb5752352f 100644 --- a/apps/user_ldap/lib/wizard.php +++ b/apps/user_ldap/lib/wizard.php @@ -75,9 +75,11 @@ class Wizard extends LDAPUtility { /** * counts entries in the LDAP directory + * * @param string $filter the LDAP search filter * @param string $type a string being either 'users' or 'groups'; - * @return int|bool + * @return bool|int + * @throws \Exception */ public function countEntries($filter, $type) { $reqs = array('ldapHost', 'ldapPort', 'ldapBase'); @@ -88,17 +90,36 @@ class Wizard extends LDAPUtility { throw new \Exception('Requirements not met', 400); } + $attr = array('dn'); // default + $limit = 1001; if($type === 'groups') { - $result = $this->access->countGroups($filter); + $result = $this->access->countGroups($filter, $attr, $limit); } else if($type === 'users') { - $result = $this->access->countUsers($filter); + $result = $this->access->countUsers($filter, $attr, $limit); + } else if ($type === 'objects') { + $result = $this->access->countObjects($limit); } else { - throw new \Exception('internal error: invald object type', 500); + throw new \Exception('internal error: invalid object type', 500); } return $result; } + /** + * formats the return value of a count operation to the string to be + * inserted. + * + * @param bool|int $count + * @return int|string + */ + private function formatCountResult($count) { + $formatted = ($count !== false) ? $count : 0; + if($formatted > 1000) { + $formatted = '> 1000'; + } + return $formatted; + } + public function countGroups() { $filter = $this->configuration->ldapGroupFilter; @@ -109,7 +130,7 @@ class Wizard extends LDAPUtility { } try { - $groupsTotal = $this->countEntries($filter, 'groups'); + $groupsTotal = $this->formatCountResult($this->countEntries($filter, 'groups')); } catch (\Exception $e) { //400 can be ignored, 500 is forwarded if($e->getCode() === 500) { @@ -117,7 +138,6 @@ class Wizard extends LDAPUtility { } return false; } - $groupsTotal = ($groupsTotal !== false) ? $groupsTotal : 0; $output = self::$l->n('%s group found', '%s groups found', $groupsTotal, array($groupsTotal)); $this->result->addChange('ldap_group_count', $output); return $this->result; @@ -130,14 +150,29 @@ class Wizard extends LDAPUtility { public function countUsers() { $filter = $this->access->getFilterForUserCount(); - $usersTotal = $this->countEntries($filter, 'users'); - $usersTotal = ($usersTotal !== false) ? $usersTotal : 0; + $usersTotal = $this->formatCountResult($this->countEntries($filter, 'users')); $output = self::$l->n('%s user found', '%s users found', $usersTotal, array($usersTotal)); $this->result->addChange('ldap_user_count', $output); return $this->result; } /** + * counts any objects in the currently set base dn + * + * @return WizardResult + * @throws \Exception + */ + public function countInBaseDN() { + // we don't need to provide a filter in this case + $total = $this->countEntries(null, 'objects'); + if($total === false) { + throw new \Exception('invalid results received'); + } + $this->result->addChange('ldap_test_base', $total); + return $this->result; + } + + /** * counts users with a specified attribute * @param string $attr * @param bool $existsCheck @@ -282,45 +317,6 @@ class Wizard extends LDAPUtility { } /** - * return the state of the Group Filter Mode - * @return WizardResult - */ - public function getGroupFilterMode() { - $this->getFilterMode('ldapGroupFilterMode'); - return $this->result; - } - - /** - * return the state of the Login Filter Mode - * @return WizardResult - */ - public function getLoginFilterMode() { - $this->getFilterMode('ldapLoginFilterMode'); - return $this->result; - } - - /** - * return the state of the User Filter Mode - * @return WizardResult - */ - public function getUserFilterMode() { - $this->getFilterMode('ldapUserFilterMode'); - return $this->result; - } - - /** - * return the state of the mode of the specified filter - * @param string $confKey contains the access key of the Configuration - */ - private function getFilterMode($confKey) { - $mode = $this->configuration->$confKey; - if(is_null($mode)) { - $mode = $this->LFILTER_MODE_ASSISTED; - } - $this->result->addChange($confKey, $mode); - } - - /** * detects the available LDAP attributes * @return array|false The instance's WizardResult instance * @throws \Exception @@ -467,8 +463,7 @@ class Wizard extends LDAPUtility { return false; } $this->configuration->setConfiguration(array('ldapGroupMemberAssocAttr' => $attribute)); - //so it will be saved on destruct - $this->result->markChange(); + $this->result->addChange('ldap_group_member_assoc_attribute', $attribute); return $this->result; } @@ -604,6 +599,41 @@ class Wizard extends LDAPUtility { } /** + * @return bool|WizardResult + * @param string $loginName + * @throws \Exception + */ + public function testLoginName($loginName) { + if(!$this->checkRequirements(array('ldapHost', + 'ldapPort', + 'ldapBase', + 'ldapLoginFilter', + ))) { + return false; + } + + $cr = $this->access->connection->getConnectionResource(); + if(!$this->ldap->isResource($cr)) { + throw new \Exception('connection error'); + } + + if(mb_strpos($this->access->connection->ldapLoginFilter, '%uid', 0, 'UTF-8') + === false) { + throw new \Exception('missing placeholder'); + } + + $users = $this->access->fetchUsersByLoginName($loginName); + if($this->ldap->errno($cr) !== 0) { + throw new \Exception($this->ldap->error($cr)); + } + $filter = \OCP\Util::mb_str_replace( + '%uid', $loginName, $this->access->connection->ldapLoginFilter, 'UTF-8'); + $this->result->addChange('ldap_test_loginname', count($users)); + $this->result->addChange('ldap_test_effective_filter', $filter); + return $this->result; + } + + /** * Tries to determine the port, requires given Host, User DN and Password * @return WizardResult|false WizardResult on success, false otherwise * @throws \Exception @@ -674,10 +704,13 @@ class Wizard extends LDAPUtility { } $dparts = explode('.', $domain); - $base2 = implode('dc=', $dparts); - if($base !== $base2 && $this->testBaseDN($base2)) { - $this->applyFind('ldap_base', $base2); - return $this->result; + while(count($dparts) > 0) { + $base2 = 'dc=' . implode(',dc=', $dparts); + if ($base !== $base2 && $this->testBaseDN($base2)) { + $this->applyFind('ldap_base', $base2); + return $this->result; + } + array_shift($dparts); } return false; @@ -720,7 +753,7 @@ class Wizard extends LDAPUtility { * @throws \Exception */ private function detectGroupMemberAssoc() { - $possibleAttrs = array('uniqueMember', 'memberUid', 'member', 'unfugasdfasdfdfa'); + $possibleAttrs = array('uniqueMember', 'memberUid', 'member'); $filter = $this->configuration->ldapGroupFilter; if(empty($filter)) { return false; @@ -730,7 +763,7 @@ class Wizard extends LDAPUtility { throw new \Exception('Could not connect to LDAP'); } $base = $this->configuration->ldapBase[0]; - $rr = $this->ldap->search($cr, $base, $filter, $possibleAttrs); + $rr = $this->ldap->search($cr, $base, $filter, $possibleAttrs, 0, 1000); if(!$this->ldap->isResource($rr)) { return false; } @@ -1114,7 +1147,8 @@ class Wizard extends LDAPUtility { //skip when the filter is a wildcard and results were found continue; } - $rr = $this->ldap->search($cr, $base, $filter, array($attr)); + // 20k limit for performance and reason + $rr = $this->ldap->search($cr, $base, $filter, array($attr), 0, 20000); if(!$this->ldap->isResource($rr)) { continue; } diff --git a/apps/user_ldap/settings.php b/apps/user_ldap/settings.php index 85bfbaa076f..d17361cdfd5 100644 --- a/apps/user_ldap/settings.php +++ b/apps/user_ldap/settings.php @@ -29,13 +29,6 @@ OC_Util::checkAdminUser(); -OCP\Util::addScript('user_ldap', 'ldapFilter'); -OCP\Util::addScript('user_ldap', 'experiencedAdmin'); -OCP\Util::addScript('user_ldap', 'settings'); -\OC_Util::addVendorScript('user_ldap', 'ui-multiselect/src/jquery.multiselect'); -OCP\Util::addStyle('user_ldap', 'settings'); -\OC_Util::addVendorStyle('user_ldap', 'ui-multiselect/jquery.multiselect'); - // fill template $tmpl = new OCP\Template('user_ldap', 'settings'); @@ -55,9 +48,9 @@ $l = \OC::$server->getL10N('user_ldap'); $wizTabs = array(); $wizTabs[] = array('tpl' => 'part.wizard-server', 'cap' => $l->t('Server')); -$wizTabs[] = array('tpl' => 'part.wizard-userfilter', 'cap' => $l->t('User Filter')); -$wizTabs[] = array('tpl' => 'part.wizard-loginfilter', 'cap' => $l->t('Login Filter')); -$wizTabs[] = array('tpl' => 'part.wizard-groupfilter', 'cap' => $l->t('Group Filter')); +$wizTabs[] = array('tpl' => 'part.wizard-userfilter', 'cap' => $l->t('Users')); +$wizTabs[] = array('tpl' => 'part.wizard-loginfilter', 'cap' => $l->t('Login Attributes')); +$wizTabs[] = array('tpl' => 'part.wizard-groupfilter', 'cap' => $l->t('Groups')); $wizTabsCount = count($wizTabs); for($i = 0; $i < $wizTabsCount; $i++) { $tab = new OCP\Template('user_ldap', $wizTabs[$i]['tpl']); diff --git a/apps/user_ldap/templates/part.settingcontrols.php b/apps/user_ldap/templates/part.settingcontrols.php index e67cea41d9c..bac00daa39f 100644 --- a/apps/user_ldap/templates/part.settingcontrols.php +++ b/apps/user_ldap/templates/part.settingcontrols.php @@ -1,5 +1,4 @@ <div class="ldapSettingControls"> - <input class="ldap_submit" value="<?php p($l->t('Save'));?>" type="submit"> <button type="button" class="ldap_action_test_connection" name="ldap_action_test_connection"> <?php p($l->t('Test Configuration'));?> </button> diff --git a/apps/user_ldap/templates/part.wizard-groupfilter.php b/apps/user_ldap/templates/part.wizard-groupfilter.php index 1953d2eaa6e..bfcfd115218 100644 --- a/apps/user_ldap/templates/part.wizard-groupfilter.php +++ b/apps/user_ldap/templates/part.wizard-groupfilter.php @@ -5,31 +5,48 @@ </p> <p> <label for="ldap_groupfilter_objectclass"> - <?php p($l->t('only those object classes:'));?> + <?php p($l->t('Only these object classes:'));?> </label> <select id="ldap_groupfilter_objectclass" multiple="multiple" - name="ldap_groupfilter_objectclass"> + name="ldap_groupfilter_objectclass" class="multiSelectPlugin"> </select> </p> <p> <label for="ldap_groupfilter_groups"> - <?php p($l->t('only from those groups:'));?> + <?php p($l->t('Only from these groups:'));?> </label> + <input type="text" class="ldapManyGroupsSupport ldapManyGroupsSearch hidden" placeholder="<?php p($l->t('Search groups'));?>" /> + <select id="ldap_groupfilter_groups" multiple="multiple" - name="ldap_groupfilter_groups"> + name="ldap_groupfilter_groups" class="multiSelectPlugin"> </select> + + </p> + <p class="ldapManyGroupsSupport hidden"> + <label></label> + <select class="ldapGroupList ldapGroupListAvailable" multiple="multiple" + title="<?php p($l->t('Available groups'));?>"></select> + <span> + <button class="ldapGroupListSelect" type="button">></button><br/> + <button class="ldapGroupListDeselect" type="button"><</button> + </span> + <select class="ldapGroupList ldapGroupListSelected" multiple="multiple" + title="<?php p($l->t('Selected groups'));?>"></select> </p> <p> - <label><a id='toggleRawGroupFilter'>↓ <?php p($l->t('Edit raw filter instead'));?></a></label> + <label><a id='toggleRawGroupFilter' class='ldapToggle'>↓ <?php p($l->t('Edit LDAP Query'));?></a></label> + </p> + <p id="ldapReadOnlyGroupFilterContainer" class="hidden ldapReadOnlyFilterContainer"> + <label><?php p($l->t('LDAP Filter:'));?></label> + <span class="ldapFilterReadOnlyElement ldapInputColElement"></span> </p> <p id="rawGroupFilterContainer" class="invisible"> - <input type="text" id="ldap_group_filter" name="ldap_group_filter" - class="lwautosave" - placeholder="<?php p($l->t('Raw LDAP filter'));?>" - title="<?php p($l->t('The filter specifies which LDAP groups shall have access to the %s instance.', $theme->getName()));?>" - /> + <textarea type="text" id="ldap_group_filter" name="ldap_group_filter" + placeholder="<?php p($l->t('Edit LDAP Query'));?>" + title="<?php p($l->t('The filter specifies which LDAP groups shall have access to the %s instance.', $theme->getName()));?>"> + </textarea> <button class="ldapGetEntryCount hidden" name="ldapGetEntryCount" type="button"> <?php p($l->t('Test Filter'));?> </button> @@ -38,7 +55,10 @@ <div class="ldapWizardInfo invisible"> </div> </p> <p class="ldap_count"> - <span id="ldap_group_count">0 <?php p($l->t('groups found'));?></span> + <button class="ldapGetEntryCount ldapGetGroupCount" name="ldapGetEntryCount" type="button"> + <?php p($l->t('Verify settings and count groups'));?> + </button> + <span id="ldap_group_count"></span> </p> <?php print_unescaped($_['wizardControls']); ?> </div> diff --git a/apps/user_ldap/templates/part.wizard-loginfilter.php b/apps/user_ldap/templates/part.wizard-loginfilter.php index 3dde46fa979..fa17a9b430b 100644 --- a/apps/user_ldap/templates/part.wizard-loginfilter.php +++ b/apps/user_ldap/templates/part.wizard-loginfilter.php @@ -1,23 +1,25 @@ <fieldset id="ldapWizard3"> <div> <p> - <?php p($l->t('Users login with this attribute:'));?> + <?php p($l->t('When logging in, %s will find the user based on the following attributes:', $theme->getName()));?> </p> <p> <label for="ldap_loginfilter_username"> - <?php p($l->t('LDAP Username:'));?> + <?php p($l->t('LDAP / AD Username:'));?> </label> <input type="checkbox" id="ldap_loginfilter_username" - name="ldap_loginfilter_username" value="1" class="lwautosave" /> + title="<?php p($l->t('Allows login against the LDAP / AD username, which is either uid or samaccountname and will be detected.'));?>" + name="ldap_loginfilter_username" value="1" /> </p> <p> <label for="ldap_loginfilter_email"> - <?php p($l->t('LDAP Email Address:'));?> + <?php p($l->t('LDAP / AD Email Address:'));?> </label> <input type="checkbox" id="ldap_loginfilter_email" - name="ldap_loginfilter_email" value="1" class="lwautosave" /> + title="<?php p($l->t('Allows login against an email attribute. Mail and mailPrimaryAddress will be allowed.'));?>" + name="ldap_loginfilter_email" value="1" /> </p> <p> <label for="ldap_loginfilter_attributes"> @@ -25,23 +27,35 @@ </label> <select id="ldap_loginfilter_attributes" multiple="multiple" - name="ldap_loginfilter_attributes"> + name="ldap_loginfilter_attributes" class="multiSelectPlugin"> </select> </p> <p> - <label><a id='toggleRawLoginFilter'>↓ <?php p($l->t('Edit raw filter instead'));?></a></label> + <label><a id='toggleRawLoginFilter' class='ldapToggle'>↓ <?php p($l->t('Edit LDAP Query'));?></a></label> + </p> + <p id="ldapReadOnlyLoginFilterContainer" class="hidden ldapReadOnlyFilterContainer"> + <label><?php p($l->t('LDAP Filter:'));?></label> + <span class="ldapFilterReadOnlyElement ldapInputColElement"></span> </p> <p id="rawLoginFilterContainer" class="invisible"> - <input type="text" id="ldap_login_filter" name="ldap_login_filter" - class="lwautosave" - placeholder="<?php p($l->t('Raw LDAP filter'));?>" - title="<?php p($l->t('Defines the filter to apply, when login is attempted. %%uid replaces the username in the login action. Example: "uid=%%uid"'));?>" - /> + <textarea type="text" id="ldap_login_filter" name="ldap_login_filter" + class="ldapFilterInputElement" + placeholder="<?php p($l->t('Edit LDAP Query'));?>" + title="<?php p($l->t('Defines the filter to apply, when login is attempted. %%uid replaces the username in the login action. Example: "uid=%%uid"'));?>"> + </textarea> </p> <p> <div class="ldapWizardInfo invisible"> </div> </p> - + <p class="ldap_verify"> + <input type="text" id="ldap_test_loginname" name="ldap_test_loginname" + placeholder="<?php p($l->t('Test Loginname'));?>" + class="ldapVerifyInput" + title="Attempts to receive a DN for the given loginname and the current login filter"/> + <button class="ldapVerifyLoginName" name="ldapTestLoginSettings" type="button"> + <?php p($l->t('Verify settings'));?> + </button> + </p> <?php print_unescaped($_['wizardControls']); ?> </div> </fieldset>
\ No newline at end of file diff --git a/apps/user_ldap/templates/part.wizard-server.php b/apps/user_ldap/templates/part.wizard-server.php index c1744143f98..3ce912fac4a 100644 --- a/apps/user_ldap/templates/part.wizard-server.php +++ b/apps/user_ldap/templates/part.wizard-server.php @@ -22,32 +22,41 @@ } } ?> - <option value="NEW"><?php p($l->t('Add Server Configuration'));?></option> </select> + <button type="button" id="ldap_action_add_configuration" + name="ldap_action_add_configuration" class="icon-add" + title="Adds a new and blank configuration"> </button> + <button type="button" id="ldap_action_copy_configuration" + name="ldap_action_copy_configuration" + class="ldapIconCopy icon-default-style" + title="Copy current configuration into new directory binding"> </button> <button type="button" id="ldap_action_delete_configuration" - name="ldap_action_delete_configuration"><?php p($l->t('Delete Configuration'));?></button> + name="ldap_action_delete_configuration" class="icon-delete" + title="Delete the current configuration"> </button> </p> <div class="hostPortCombinator"> <div class="tablerow"> <div class="tablecell"> <div class="table"> - <input type="text" class="host tablecell lwautosave" id="ldap_host" + <input type="text" class="host" id="ldap_host" name="ldap_host" placeholder="<?php p($l->t('Host'));?>" title="<?php p($l->t('You can omit the protocol, except you require SSL. Then start with ldaps://'));?>" /> <span> <input type="number" id="ldap_port" name="ldap_port" - class="lwautosave" placeholder="<?php p($l->t('Port'));?>" /> + <button class="ldapDetectPort" name="ldapDetectPort" type="button"> + <?php p($l->t('Detect Port'));?> + </button> </span> </div> </div> </div> <div class="tablerow"> <input type="text" id="ldap_dn" name="ldap_dn" - class="tablecell lwautosave" + class="tablecell" placeholder="<?php p($l->t('User DN'));?>" autocomplete="off" title="<?php p($l->t('The DN of the client user with which the bind shall be done, e.g. uid=agent,dc=example,dc=com. For anonymous access, leave DN and Password empty.'));?>" /> @@ -55,7 +64,7 @@ <div class="tablerow"> <input type="password" id="ldap_agent_password" - class="tablecell lwautosave" name="ldap_agent_password" + class="tablecell" name="ldap_agent_password" placeholder="<?php p($l->t('Password'));?>" autocomplete="off" title="<?php p($l->t('For anonymous access, leave DN and Password empty.'));?>" /> @@ -63,15 +72,21 @@ <div class="tablerow"> <textarea id="ldap_base" name="ldap_base" - class="tablecell lwautosave" + class="tablecell" placeholder="<?php p($l->t('One Base DN per line'));?>" title="<?php p($l->t('You can specify Base DN for users and groups in the Advanced tab'));?>"> </textarea> + <button class="ldapDetectBase" name="ldapDetectBase" type="button"> + <?php p($l->t('Detect Base DN'));?> + </button> + <button class="ldapTestBase" name="ldapTestBase" type="button"> + <?php p($l->t('Test Base DN'));?> + </button> </div> <div class="tablerow left"> <input type="checkbox" id="ldap_experienced_admin" value="1" - name="ldap_experienced_admin" class="tablecell lwautosave" + name="ldap_experienced_admin" class="tablecell" title="<?php p($l->t('Avoids automatic LDAP requests. Better for bigger setups, but requires some LDAP knowledge.'));?>" /> <label for="ldap_experienced_admin" class="tablecell"> diff --git a/apps/user_ldap/templates/part.wizard-userfilter.php b/apps/user_ldap/templates/part.wizard-userfilter.php index 99a6e75370b..691c41a66a6 100644 --- a/apps/user_ldap/templates/part.wizard-userfilter.php +++ b/apps/user_ldap/templates/part.wizard-userfilter.php @@ -5,40 +5,61 @@ </p> <p> <label for="ldap_userfilter_objectclass"> - <?php p($l->t('only those object classes:'));?> + <?php p($l->t('Only these object classes:'));?> </label> <select id="ldap_userfilter_objectclass" multiple="multiple" - name="ldap_userfilter_objectclass"> + name="ldap_userfilter_objectclass" class="multiSelectPlugin"> </select> </p> <p> + <label></label> + <span class="ldapInputColElement"><?php p($l->t('The most common object classes for users are organizationalPerson, person, user, and inetOrgPerson. If you are not sure which object class to select, please consult your directory admin.'));?></span> + </p> + <p> <label for="ldap_userfilter_groups"> - <?php p($l->t('only from those groups:'));?> + <?php p($l->t('Only from these groups:'));?> </label> + <input type="text" class="ldapManyGroupsSupport ldapManyGroupsSearch hidden" placeholder="<?php p($l->t('Search groups'));?>" /> + <select id="ldap_userfilter_groups" multiple="multiple" - name="ldap_userfilter_groups"> + name="ldap_userfilter_groups" class="multiSelectPlugin"> </select> </p> + <p class="ldapManyGroupsSupport hidden"> + <label></label> + <select class="ldapGroupList ldapGroupListAvailable" multiple="multiple" + title="<?php p($l->t('Available groups'));?>"></select> + <span> + <button class="ldapGroupListSelect" type="button">></button><br/> + <button class="ldapGroupListDeselect" type="button"><</button> + </span> + <select class="ldapGroupList ldapGroupListSelected" multiple="multiple" + title="<?php p($l->t('Selected groups'));?>"></select> + </p> <p> - <label><a id='toggleRawUserFilter'>↓ <?php p($l->t('Edit raw filter instead'));?></a></label> - </p> - <p id="rawUserFilterContainer" class="invisible"> - <input type="text" id="ldap_userlist_filter" name="ldap_userlist_filter" - class="lwautosave" - placeholder="<?php p($l->t('Raw LDAP filter'));?>" - title="<?php p($l->t('The filter specifies which LDAP users shall have access to the %s instance.', $theme->getName()));?>" - /> - <button class="ldapGetEntryCount hidden" name="ldapGetEntryCount" type="button"> - <?php p($l->t('Test Filter'));?> - </button> + <label><a id='toggleRawUserFilter' class='ldapToggle'>↓ <?php p($l->t('Edit LDAP Query'));?></a></label> + </p> + <p id="ldapReadOnlyUserFilterContainer" class="hidden ldapReadOnlyFilterContainer"> + <label><?php p($l->t('LDAP Filter:'));?></label> + <span class="ldapFilterReadOnlyElement ldapInputColElement"></span> + </p> + <p id="rawUserFilterContainer"> + <textarea type="text" id="ldap_userlist_filter" name="ldap_userlist_filter" + class="ldapFilterInputElement" + placeholder="<?php p($l->t('Edit LDAP Query'));?>" + title="<?php p($l->t('The filter specifies which LDAP users shall have access to the %s instance.', $theme->getName()));?>"> + </textarea> </p> <p> <div class="ldapWizardInfo invisible"> </div> </p> <p class="ldap_count"> - <span id="ldap_user_count">0 <?php p($l->t('users found'));?></span> + <button class="ldapGetEntryCount ldapGetUserCount" name="ldapGetEntryCount" type="button"> + <?php p($l->t('Verify settings and count users'));?> + </button> + <span id="ldap_user_count"></span> </p> <?php print_unescaped($_['wizardControls']); ?> </div> diff --git a/apps/user_ldap/templates/settings.php b/apps/user_ldap/templates/settings.php index 6aa2183726b..f40eba005d8 100644 --- a/apps/user_ldap/templates/settings.php +++ b/apps/user_ldap/templates/settings.php @@ -1,3 +1,56 @@ +<?php + +vendor_script('user_ldap', 'ui-multiselect/src/jquery.multiselect'); + +vendor_style('user_ldap', 'ui-multiselect/jquery.multiselect'); + +script('user_ldap', [ + 'wizard/controller', + 'wizard/configModel', + 'wizard/view', + 'wizard/wizardObject', + 'wizard/wizardTabGeneric', + 'wizard/wizardTabElementary', + 'wizard/wizardTabAbstractFilter', + 'wizard/wizardTabUserFilter', + 'wizard/wizardTabLoginFilter', + 'wizard/wizardTabGroupFilter', + 'wizard/wizardTabAdvanced', + 'wizard/wizardTabExpert', + 'wizard/wizardDetectorQueue', + 'wizard/wizardDetectorGeneric', + 'wizard/wizardDetectorPort', + 'wizard/wizardDetectorBaseDN', + 'wizard/wizardDetectorFeatureAbstract', + 'wizard/wizardDetectorUserObjectClasses', + 'wizard/wizardDetectorGroupObjectClasses', + 'wizard/wizardDetectorGroupsForUsers', + 'wizard/wizardDetectorGroupsForGroups', + 'wizard/wizardDetectorSimpleRequestAbstract', + 'wizard/wizardDetectorFilterUser', + 'wizard/wizardDetectorFilterLogin', + 'wizard/wizardDetectorFilterGroup', + 'wizard/wizardDetectorUserCount', + 'wizard/wizardDetectorGroupCount', + 'wizard/wizardDetectorEmailAttribute', + 'wizard/wizardDetectorUserDisplayNameAttribute', + 'wizard/wizardDetectorUserGroupAssociation', + 'wizard/wizardDetectorAvailableAttributes', + 'wizard/wizardDetectorTestAbstract', + 'wizard/wizardDetectorTestLoginName', + 'wizard/wizardDetectorTestBaseDN', + 'wizard/wizardDetectorTestConfiguration', + 'wizard/wizardDetectorClearUserMappings', + 'wizard/wizardDetectorClearGroupMappings', + 'wizard/wizardFilterOnType', + 'wizard/wizardFilterOnTypeFactory', + 'wizard/wizard' +]); + +style('user_ldap', 'settings'); + +?> + <form id="ldap" class="section" action="#" method="post"> <h2><?php p($l->t('LDAP')); ?></h2> @@ -65,5 +118,6 @@ <?php print_unescaped($_['settingControls']); ?> </fieldset> </div> - + <!-- Spinner Template --> + <img class="ldapSpinner hidden" src="<?php p(\OCP\Util::imagePath('core', 'loading.gif')); ?>"> </form> diff --git a/apps/user_ldap/tests/user_ldap.php b/apps/user_ldap/tests/user_ldap.php index fa3afe9c511..b9beed1d35a 100644 --- a/apps/user_ldap/tests/user_ldap.php +++ b/apps/user_ldap/tests/user_ldap.php @@ -108,12 +108,6 @@ class Test_User_Ldap_Direct extends \Test\TestCase { * @return void */ private function prepareAccessForCheckPassword(&$access, $noDisplayName = false) { - $access->expects($this->once()) - ->method('escapeFilterPart') - ->will($this->returnCallback(function($uid) { - return $uid; - })); - $access->connection->expects($this->any()) ->method('__get') ->will($this->returnCallback(function($name) { @@ -132,6 +126,15 @@ class Test_User_Ldap_Direct extends \Test\TestCase { return array(); })); + $access->expects($this->any()) + ->method('fetchUsersByLoginName') + ->will($this->returnCallback(function($uid) { + if($uid === 'roland') { + return array(array('dn' => 'dnOfRoland,dc=test')); + } + return array(); + })); + $retVal = 'gunslinger'; if($noDisplayName === true) { $retVal = false; diff --git a/apps/user_ldap/user_ldap.php b/apps/user_ldap/user_ldap.php index ea1371c14d3..54e14c093f3 100644 --- a/apps/user_ldap/user_ldap.php +++ b/apps/user_ldap/user_ldap.php @@ -79,14 +79,10 @@ class USER_LDAP extends BackendUtility implements \OCP\IUserBackend, \OCP\UserIn * Check if the password is correct without logging in the user */ public function checkPassword($uid, $password) { - $uid = $this->access->escapeFilterPart($uid); - //find out dn of the user name $attrs = array($this->access->connection->ldapUserDisplayName, 'dn', 'uid', 'samaccountname'); - $filter = \OCP\Util::mb_str_replace( - '%uid', $uid, $this->access->connection->ldapLoginFilter, 'UTF-8'); - $users = $this->access->fetchListOfUsers($filter, $attrs); + $users = $this->access->fetchUsersByLoginName($uid, $attrs); if(count($users) < 1) { return false; } diff --git a/config/config.sample.php b/config/config.sample.php index 60932ab7d9b..e3b81f69f6b 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -577,6 +577,15 @@ $CONFIG = array( 'appstoreurl' => 'https://api.owncloud.com/v1', /** + * Whether to show experimental apps in the appstore interface + * + * Experimental apps are not checked for security issues and are new or known + * to be unstable and under heavy development. Installing these can cause data + * loss or security breaches. + */ +'appstore.experimental.enabled' => false, + +/** * Use the ``apps_paths`` parameter to set the location of the Apps directory, * which should be scanned for available apps, and where user-specific apps * should be installed from the Apps store. The ``path`` defines the absolute @@ -620,12 +629,12 @@ $CONFIG = array( * The maximum width, in pixels, of a preview. A value of ``null`` means there * is no limit. */ -'preview_max_x' => null, +'preview_max_x' => 2048, /** * The maximum height, in pixels, of a preview. A value of ``null`` means there * is no limit. */ -'preview_max_y' => null, +'preview_max_y' => 2048, /** * If a lot of small pictures are stored on the ownCloud instance and the * preview system generates blurry previews, you might want to consider setting diff --git a/console.php b/console.php index a4d829f683a..7536908a5c1 100644 --- a/console.php +++ b/console.php @@ -24,6 +24,7 @@ */ use Symfony\Component\Console\Application; +use Symfony\Component\Console\Input\ArgvInput; define('OC_CONSOLE', 1); @@ -71,6 +72,18 @@ try { } else { echo "ownCloud is not installed - only a limited number of commands are available" . PHP_EOL; } + $input = new ArgvInput(); + if ($input->getFirstArgument() !== 'check') { + $errors = \OC_Util::checkServer(\OC::$server->getConfig()); + if (!empty($errors)) { + foreach ($errors as $error) { + echo $error['error'] . "\n"; + echo $error['hint'] . "\n\n"; + } + exit(1); + } + } + $application->run(); } catch (Exception $ex) { echo "An unhandled exception has been thrown:" . PHP_EOL; diff --git a/core/ajax/share.php b/core/ajax/share.php index 05756fc1c8b..bc83c41642c 100644 --- a/core/ajax/share.php +++ b/core/ajax/share.php @@ -255,6 +255,14 @@ if (isset($_POST['action']) && isset($_POST['itemType']) && isset($_POST['itemSo $usergroups = OC_Group::getUserGroups(OC_User::getUser()); $groups = array_intersect($groups, $usergroups); } + + $sharedUsers = []; + $sharedGroups = []; + if (isset($_GET['itemShares'])) { + $sharedUsers = isset($_GET['itemShares'][OCP\Share::SHARE_TYPE_USER]) ? $_GET['itemShares'][OCP\Share::SHARE_TYPE_USER] : []; + $sharedGroups = isset($_GET['itemShares'][OCP\Share::SHARE_TYPE_GROUP]) ? $_GET['itemShares'][OCP\Share::SHARE_TYPE_GROUP] : []; + } + $count = 0; $users = array(); $limit = 0; @@ -266,8 +274,13 @@ if (isset($_POST['action']) && isset($_POST['itemType']) && isset($_POST['itemSo } else { $users = OC_User::getDisplayNames((string)$_GET['search'], $limit, $offset); } + $offset += $limit; foreach ($users as $uid => $displayName) { + if (in_array($uid, $sharedUsers)) { + continue; + } + if ((!isset($_GET['itemShares']) || !is_array((string)$_GET['itemShares'][OCP\Share::SHARE_TYPE_USER]) || !in_array($uid, (string)$_GET['itemShares'][OCP\Share::SHARE_TYPE_USER])) @@ -288,6 +301,10 @@ if (isset($_POST['action']) && isset($_POST['itemType']) && isset($_POST['itemSo $l = \OC::$server->getL10N('core'); foreach ($groups as $group) { + if (in_array($group, $sharedGroups)) { + continue; + } + if ($count < 15) { if (!isset($_GET['itemShares']) || !isset($_GET['itemShares'][OCP\Share::SHARE_TYPE_GROUP]) diff --git a/core/command/app/listapps.php b/core/command/app/listapps.php index dbb04c41eed..37a1d645ed4 100644 --- a/core/command/app/listapps.php +++ b/core/command/app/listapps.php @@ -23,21 +23,23 @@ namespace OC\Core\Command\App; -use Symfony\Component\Console\Command\Command; +use OC\Core\Command\Base; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class ListApps extends Command { +class ListApps extends Base { protected function configure() { + parent::configure(); + $this ->setName('app:list') - ->setDescription('List all available apps'); + ->setDescription('List all available apps') + ; } protected function execute(InputInterface $input, OutputInterface $output) { $apps = \OC_App::getAllApps(); - $enabledApps = array(); - $disabledApps = array(); + $enabledApps = $disabledApps = []; $versions = \OC_App::getAppVersions(); //sort enabled apps above disabled apps @@ -49,15 +51,39 @@ class ListApps extends Command { } } + $apps = ['enabled' => [], 'disabled' => []]; + sort($enabledApps); - sort($disabledApps); - $output->writeln('Enabled:'); foreach ($enabledApps as $app) { - $output->writeln(' - ' . $app . (isset($versions[$app]) ? ' (' . $versions[$app] . ')' : '')); + $apps['enabled'][$app] = (isset($versions[$app])) ? $versions[$app] : ''; } - $output->writeln('Disabled:'); + + sort($disabledApps); foreach ($disabledApps as $app) { - $output->writeln(' - ' . $app . (isset($versions[$app]) ? ' (' . $versions[$app] . ')' : '')); + $apps['disabled'][$app] = (isset($versions[$app])) ? $versions[$app] : ''; + } + + $this->writeAppList($input, $output, $apps); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @param array $items + */ + protected function writeAppList(InputInterface $input, OutputInterface $output, $items) { + switch ($input->getOption('output')) { + case 'plain': + $output->writeln('Enabled:'); + parent::writeArrayInOutputFormat($input, $output, $items['enabled']); + + $output->writeln('Disabled:'); + parent::writeArrayInOutputFormat($input, $output, $items['disabled']); + break; + + default: + parent::writeArrayInOutputFormat($input, $output, $items); + break; } } } diff --git a/core/command/base.php b/core/command/base.php new file mode 100644 index 00000000000..c2d5cf97f02 --- /dev/null +++ b/core/command/base.php @@ -0,0 +1,62 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Core\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class Base extends Command { + protected function configure() { + $this + ->addOption( + 'output', + null, + InputOption::VALUE_OPTIONAL, + 'Output format (plain, json or json_pretty, default is plain)', + 'plain' + ) + ; + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @param array $items + */ + protected function writeArrayInOutputFormat(InputInterface $input, OutputInterface $output, $items) { + switch ($input->getOption('output')) { + case 'json': + $output->writeln(json_encode($items)); + break; + case 'json_pretty': + $output->writeln(json_encode($items, JSON_PRETTY_PRINT)); + break; + default: + foreach ($items as $key => $item) { + $output->writeln(' - ' . (!is_int($key) ? $key . ': ' : '') . $item); + } + break; + } + } +} diff --git a/core/command/check.php b/core/command/check.php new file mode 100644 index 00000000000..ddfe9b73bba --- /dev/null +++ b/core/command/check.php @@ -0,0 +1,41 @@ +<?php + +namespace OC\Core\Command; + +use OCP\IConfig; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Check extends Base { + /** + * @var IConfig + */ + private $config; + + public function __construct(IConfig $config) { + parent::__construct(); + $this->config = $config; + } + + protected function configure() { + parent::configure(); + + $this + ->setName('check') + ->setDescription('check dependencies of the server environment') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) { + $errors = \OC_Util::checkServer($this->config); + if (!empty($errors)) { + $errors = array_map(function($item) { + return (string) $item['error']; + }, $errors); + + $this->writeArrayInOutputFormat($input, $output, $errors); + return 1; + } + return 0; + } +} diff --git a/core/command/status.php b/core/command/status.php index 8c6653a8910..3859f69febc 100644 --- a/core/command/status.php +++ b/core/command/status.php @@ -22,12 +22,13 @@ namespace OC\Core\Command; -use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class Status extends Command { +class Status extends Base { protected function configure() { + parent::configure(); + $this ->setName('status') ->setDescription('show some status information') @@ -41,6 +42,7 @@ class Status extends Command { 'versionstring' => \OC_Util::getVersionString(), 'edition' => \OC_Util::getEditionString(), ); - print_r($values); + + $this->writeArrayInOutputFormat($input, $output, $values); } } diff --git a/core/command/user/add.php b/core/command/user/add.php index 1ae0ffbe2ad..60c70bf13dd 100644 --- a/core/command/user/add.php +++ b/core/command/user/add.php @@ -58,10 +58,10 @@ class Add extends Command { 'User ID used to login (must only contain a-z, A-Z, 0-9, -, _ and @)' ) ->addOption( - 'password', - 'p', - InputOption::VALUE_OPTIONAL, - '' + 'password-from-env', + null, + InputOption::VALUE_NONE, + 'read password from environment variable OC_PASS' ) ->addOption( 'display-name', @@ -84,14 +84,33 @@ class Add extends Command { return 1; } - $password = $input->getOption('password'); - while (!$password) { - $question = new Question('Please enter a non-empty password:'); - $question->setHidden(true); - $question->setHiddenFallback(false); + if ($input->getOption('password-from-env')) { + $password = getenv('OC_PASS'); + if (!$password) { + $output->writeln('<error>--password-from-env given, but OC_PASS is empty!</error>'); + return 1; + } + } elseif ($input->isInteractive()) { + /** @var $dialog \Symfony\Component\Console\Helper\DialogHelper */ + $dialog = $this->getHelperSet()->get('dialog'); + $password = $dialog->askHiddenResponse( + $output, + '<question>Enter password: </question>', + false + ); + $confirm = $dialog->askHiddenResponse( + $output, + '<question>Confirm password: </question>', + false + ); - $helper = $this->getHelper('question'); - $password = $helper->ask($input, $output, $question); + if ($password !== $confirm) { + $output->writeln("<error>Passwords did not match!</error>"); + return 1; + } + } else { + $output->writeln("<error>Interactive input or --password-from-env is needed for entering a password!</error>"); + return 1; } $user = $this->userManager->createUser( diff --git a/core/command/user/resetpassword.php b/core/command/user/resetpassword.php index 3afbfeeb9b9..3e16c8f79a5 100644 --- a/core/command/user/resetpassword.php +++ b/core/command/user/resetpassword.php @@ -26,6 +26,7 @@ namespace OC\Core\Command\User; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class ResetPassword extends Command { @@ -47,6 +48,12 @@ class ResetPassword extends Command { InputArgument::REQUIRED, 'Username to reset password' ) + ->addOption( + 'password-from-env', + null, + InputOption::VALUE_NONE, + 'read password from environment variable OC_PASS' + ) ; } @@ -60,7 +67,13 @@ class ResetPassword extends Command { return 1; } - if ($input->isInteractive()) { + if ($input->getOption('password-from-env')) { + $password = getenv('OC_PASS'); + if (!$password) { + $output->writeln('<error>--password-from-env given, but OC_PASS is empty!</error>'); + return 1; + } + } elseif ($input->isInteractive()) { /** @var $dialog \Symfony\Component\Console\Helper\DialogHelper */ $dialog = $this->getHelperSet()->get('dialog'); @@ -84,20 +97,20 @@ class ResetPassword extends Command { false ); - if ($password === $confirm) { - $success = $user->setPassword($password); - if ($success) { - $output->writeln("<info>Successfully reset password for " . $username . "</info>"); - } else { - $output->writeln("<error>Error while resetting password!</error>"); - return 1; - } - } else { + if ($password !== $confirm) { $output->writeln("<error>Passwords did not match!</error>"); return 1; } } else { - $output->writeln("<error>Interactive input is needed for entering a new password!</error>"); + $output->writeln("<error>Interactive input or --password-from-env is needed for entering a new password!</error>"); + return 1; + } + + $success = $user->setPassword($password); + if ($success) { + $output->writeln("<info>Successfully reset password for " . $username . "</info>"); + } else { + $output->writeln("<error>Error while resetting password!</error>"); return 1; } } diff --git a/core/css/apps.css b/core/css/apps.css index e8c60bd4773..9b662cc31af 100644 --- a/core/css/apps.css +++ b/core/css/apps.css @@ -457,11 +457,16 @@ width: 100%; padding: 0; margin: 0; - background-color: transparent; background-image: url('../img/actions/settings.svg'); - background-position: 10px center; background-repeat: no-repeat; + background-color: transparent; + background-image: url('../img/actions/settings.svg'); + background-position: 14px center; + background-repeat: no-repeat; box-shadow: none; border: 0; border-radius: 0; + text-align: left; + padding-left: 42px; + font-weight: normal; } .settings-button:hover, .settings-button:focus { diff --git a/core/css/icons.css b/core/css/icons.css index ecf6b17995d..0f602515883 100644 --- a/core/css/icons.css +++ b/core/css/icons.css @@ -79,6 +79,9 @@ .icon-info { background-image: url('../img/actions/info.svg'); } +.icon-info-white { + background-image: url('../img/actions/info-white.svg'); +} .icon-logout { background-image: url('../img/actions/logout.svg'); diff --git a/core/css/share.css b/core/css/share.css index 448f0bac239..bcfd90e4204 100644 --- a/core/css/share.css +++ b/core/css/share.css @@ -37,8 +37,12 @@ display: none !important; } +#dropdown .shareWithRemoteInfo { + padding: 11px 20px; +} + #dropdown .avatar { - margin-right: 2px; + margin-right: 8px; display: inline-block; overflow: hidden; vertical-align: middle; @@ -108,7 +112,8 @@ a.unshare { } #dropdown input[type="text"],#dropdown input[type="password"] { - width:90%; + width: 86%; + margin-left: 7px; } #dropdown form { diff --git a/core/css/styles.css b/core/css/styles.css index cbce78c525b..3df2abd49d6 100644 --- a/core/css/styles.css +++ b/core/css/styles.css @@ -409,7 +409,9 @@ input[type="submit"].enabled { padding: 5px; } -#body-login div.buttons { text-align:center; } +#body-login div.buttons { + text-align: center; +} #body-login p.info { width: 22em; margin: 0 auto; @@ -420,14 +422,26 @@ input[type="submit"].enabled { padding: 13px; margin: -13px; } -#body-login #submit.login { margin-right:7px; } /* quick fix for log in button not being aligned with input fields, should be properly fixed by input field width later */ - -#body-login form { width:22em; margin:2em auto 2em; padding:0; } +/* quick fix for log in button not being aligned with input fields, should be properly fixed by input field width later */ +#body-login #submit.login { + margin-right: 7px; +} +#body-login form { + width: 22em; + margin: 2em auto 2em; + padding: 0; +} #body-login form fieldset { margin-bottom: 20px; text-align: left; } -#body-login form #adminaccount { margin-bottom:15px; } +#body-login form #sqliteInformation { + margin-top: -20px; + margin-bottom: 20px; +} +#body-login form #adminaccount { + margin-bottom: 15px; +} #body-login form fieldset legend, #datadirContent label { width: 100%; } @@ -445,6 +459,9 @@ input[type="submit"].enabled { vertical-align: bottom; /* adjust position of Advanced dropdown arrow */ margin-left: -4px; } +#body-login .icon-info-white { + padding: 10px; +} /* strengthify wrapper */ #body-login .strengthify-wrapper { @@ -618,7 +635,9 @@ label.infield { /* Warnings and errors are the same */ -#body-login .warning, #body-login .update, #body-login .error { +#body-login .warning, +#body-login .update, +#body-login .error { display: block; padding: 10px; background-color: rgba(0,0,0,.3); @@ -670,7 +689,7 @@ label.infield { } .warning-input { - border-color: #cc3333 !important; + border-color: #ce3702 !important; } /* Fixes for log in page, TODO should be removed some time */ diff --git a/core/img/actions/info-white.png b/core/img/actions/info-white.png Binary files differnew file mode 100644 index 00000000000..670d7309c4e --- /dev/null +++ b/core/img/actions/info-white.png diff --git a/core/img/actions/info-white.svg b/core/img/actions/info-white.svg new file mode 100644 index 00000000000..d1f9ddb78cf --- /dev/null +++ b/core/img/actions/info-white.svg @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> + <rect style="color:#000000" fill-opacity="0" height="97.986" width="163.31" y="-32.993" x="-62.897"/> + <path d="m4.9999 7.4745c0.1553 0.3811 0.3254 0.6881 0.6445 0.2459 0.4066-0.2685 1.7587-1.4279 1.6616-0.3421-0.3681 2.0169-0.8342 4.0167-1.1711 6.0387-0.3916 1.115 0.635 2.068 1.6379 1.312 1.0779-0.503 1.9915-1.288 2.9275-2.012-0.144-0.322-0.25-0.789-0.596-0.346-0.4687 0.239-1.4695 1.317-1.6967 0.471 0.3154-2.181 0.9755-4.2953 1.3654-6.4616 0.3973-1.0049-0.3645-2.2233-1.3997-1.3634-1.2565 0.6173-2.2895 1.5844-3.3734 2.4575zm4.4593-7.4718c-1.3075-0.017336-1.9056 2.1455-0.6427 2.6795 1.0224 0.378 2.0768-0.7138 1.7898-1.7504-0.098-0.54186-0.598-0.96979-1.1471-0.92912h-0.000001z" fill="#fff"/> +</svg> diff --git a/core/img/actions/info.png b/core/img/actions/info.png Binary files differindex cb25905a5c5..b280a019ab4 100644 --- a/core/img/actions/info.png +++ b/core/img/actions/info.png diff --git a/core/img/actions/info.svg b/core/img/actions/info.svg index 527c1d3dbd1..3be67826423 100644 --- a/core/img/actions/info.svg +++ b/core/img/actions/info.svg @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> <rect style="color:#000000" fill-opacity="0" height="97.986" width="163.31" y="-32.993" x="-62.897"/> - <path opacity=".7" d="m4.9999 7.4745c0.1553 0.3811 0.3254 0.6881 0.6445 0.2459 0.4066-0.2685 1.7587-1.4279 1.6616-0.3421-0.3681 2.0169-0.8342 4.0167-1.1711 6.0387-0.3916 1.115 0.635 2.068 1.6379 1.312 1.0779-0.503 1.9915-1.288 2.9275-2.012-0.144-0.322-0.25-0.789-0.596-0.346-0.4687 0.239-1.4695 1.317-1.6967 0.471 0.3154-2.181 0.9755-4.2953 1.3654-6.4616 0.3973-1.0049-0.3645-2.2233-1.3997-1.3634-1.2565 0.6173-2.2895 1.5844-3.3734 2.4575zm4.4593-7.4718c-1.3075-0.017336-1.9056 2.1455-0.6427 2.6795 1.0224 0.378 2.0768-0.7138 1.7898-1.7504-0.098-0.54186-0.598-0.96979-1.1471-0.92912h-0.000001z" fill="#1e1e1e"/> + <path opacity=".5" d="m4.9999 7.4745c0.1553 0.3811 0.3254 0.6881 0.6445 0.2459 0.4066-0.2685 1.7587-1.4279 1.6616-0.3421-0.3681 2.0169-0.8342 4.0167-1.1711 6.0387-0.3916 1.115 0.635 2.068 1.6379 1.312 1.0779-0.503 1.9915-1.288 2.9275-2.012-0.144-0.322-0.25-0.789-0.596-0.346-0.4687 0.239-1.4695 1.317-1.6967 0.471 0.3154-2.181 0.9755-4.2953 1.3654-6.4616 0.3973-1.0049-0.3645-2.2233-1.3997-1.3634-1.2565 0.6173-2.2895 1.5844-3.3734 2.4575zm4.4593-7.4718c-1.3075-0.017336-1.9056 2.1455-0.6427 2.6795 1.0224 0.378 2.0768-0.7138 1.7898-1.7504-0.098-0.54186-0.598-0.96979-1.1471-0.92912h-0.000001z"/> </svg> diff --git a/core/img/actions/search.png b/core/img/actions/search.png Binary files differindex 82ebac97255..5f4767a6f46 100644 --- a/core/img/actions/search.png +++ b/core/img/actions/search.png diff --git a/core/img/actions/search.svg b/core/img/actions/search.svg index 7e7a41a2ea7..6024607dd0a 100644 --- a/core/img/actions/search.svg +++ b/core/img/actions/search.svg @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> <rect style="color:#000000" fill-opacity="0" height="97.986" width="163.31" y="-32.993" x="-62.897"/> - <path opacity=".7" style="color:#000000" d="m6 1c-2.7614 0-5 2.2386-5 5s2.2386 5 5 5c0.98478 0 1.8823-0.28967 2.6562-0.78125l4.4688 4.625c0.09558 0.10527 0.22619 0.16452 0.375 0.15625 0.14882-0.0083 0.3031-0.07119 0.40625-0.1875l0.9375-1.0625c0.19194-0.22089 0.19549-0.53592 0-0.71875l-4.594-4.406c0.478-0.7663 0.75-1.6555 0.75-2.625 0-2.7614-2.2386-5-5-5zm0 2c1.6569 0 3 1.3431 3 3s-1.3431 3-3 3-3-1.3431-3-3 1.3431-3 3-3z" fill="#1e1e1e"/> + <path opacity=".5" style="color:#000000" d="m6 1c-2.7614 0-5 2.2386-5 5s2.2386 5 5 5c0.98478 0 1.8823-0.28967 2.6562-0.78125l4.4688 4.625c0.09558 0.10527 0.22619 0.16452 0.375 0.15625 0.14882-0.0083 0.3031-0.07119 0.40625-0.1875l0.9375-1.0625c0.19194-0.22089 0.19549-0.53592 0-0.71875l-4.594-4.406c0.478-0.7663 0.75-1.6555 0.75-2.625 0-2.7614-2.2386-5-5-5zm0 2c1.6569 0 3 1.3431 3 3s-1.3431 3-3 3-3-1.3431-3-3 1.3431-3 3-3z"/> </svg> diff --git a/core/img/actions/settings.png b/core/img/actions/settings.png Binary files differindex 28f01bc7927..3ab939ca37a 100644 --- a/core/img/actions/settings.png +++ b/core/img/actions/settings.png diff --git a/core/img/actions/settings.svg b/core/img/actions/settings.svg index 51bd7460389..251bd54c19d 100644 --- a/core/img/actions/settings.svg +++ b/core/img/actions/settings.svg @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/"> - <g opacity=".7" transform="translate(0 -.056)" fill="#1e1e1e"> - <path fill="#1e1e1e" display="block" d="m6.9375 0.056c-0.2484 0-0.4375 0.18908-0.4375 0.4375v1.25c-0.5539 0.1422-1.0512 0.3719-1.5312 0.6563l-0.9063-0.9063c-0.17566-0.17566-0.44934-0.17566-0.625 0l-1.5 1.5c-0.17566 0.17566-0.17566 0.44934 0 0.625l0.9063 0.9063c-0.2844 0.48-0.5141 0.9773-0.6563 1.5312h-1.25c-0.24842 0-0.4375 0.1891-0.4375 0.4375v2.125c1e-8 0.24842 0.18908 0.4375 0.4375 0.4375h1.25c0.1422 0.5539 0.37188 1.0512 0.65625 1.5312l-0.9063 0.907c-0.17566 0.17566-0.17566 0.44934 0 0.625l1.5 1.5c0.17566 0.17566 0.44934 0.17566 0.625 0l0.9063-0.907c0.48 0.285 0.9773 0.514 1.5312 0.656v1.25c1e-7 0.24842 0.18908 0.4375 0.4375 0.4375h2.125c0.2484 0 0.4375-0.189 0.4375-0.438v-1.25c0.5539-0.1422 1.0512-0.37188 1.5312-0.65625l0.90625 0.90625c0.17566 0.17566 0.44934 0.17566 0.625 0l1.5-1.5c0.17566-0.17566 0.17566-0.44934 0-0.625l-0.906-0.906c0.285-0.48 0.514-0.9771 0.656-1.531h1.25c0.249 0 0.438-0.1891 0.438-0.4375v-2.125c0-0.2484-0.189-0.4375-0.438-0.4375h-1.25c-0.142-0.5539-0.371-1.0512-0.656-1.5312l0.906-0.9063c0.17566-0.17566 0.17566-0.44934 0-0.625l-1.5-1.5c-0.17566-0.17566-0.44934-0.17566-0.625 0l-0.906 0.9063c-0.48-0.2844-0.977-0.5141-1.531-0.6563v-1.25c0-0.24842-0.1891-0.4375-0.4375-0.4375zm1.0625 4.1573c1.8451 0 3.3427 1.4975 3.3427 3.3427 0 1.8451-1.4975 3.3427-3.3427 3.3427-1.8451 0-3.3427-1.4979-3.3427-3.343s1.4976-3.3427 3.3427-3.3427z"/> +<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> + <g opacity=".5" transform="translate(0 -.056)"> + <path d="m6.9375 0.056c-0.2484 0-0.4375 0.18908-0.4375 0.4375v1.25c-0.5539 0.1422-1.0512 0.3719-1.5312 0.6563l-0.9063-0.9063c-0.17566-0.17566-0.44934-0.17566-0.625 0l-1.5 1.5c-0.17566 0.17566-0.17566 0.44934 0 0.625l0.9063 0.9063c-0.2844 0.48-0.5141 0.9773-0.6563 1.5312h-1.25c-0.24842 0-0.4375 0.1891-0.4375 0.4375v2.125c1e-8 0.24842 0.18908 0.4375 0.4375 0.4375h1.25c0.1422 0.5539 0.37188 1.0512 0.65625 1.5312l-0.9063 0.907c-0.17566 0.17566-0.17566 0.44934 0 0.625l1.5 1.5c0.17566 0.17566 0.44934 0.17566 0.625 0l0.9063-0.907c0.48 0.285 0.9773 0.514 1.5312 0.656v1.25c1e-7 0.24842 0.18908 0.4375 0.4375 0.4375h2.125c0.2484 0 0.4375-0.189 0.4375-0.438v-1.25c0.5539-0.1422 1.0512-0.37188 1.5312-0.65625l0.90625 0.90625c0.17566 0.17566 0.44934 0.17566 0.625 0l1.5-1.5c0.17566-0.17566 0.17566-0.44934 0-0.625l-0.906-0.906c0.285-0.48 0.514-0.9771 0.656-1.531h1.25c0.249 0 0.438-0.1891 0.438-0.4375v-2.125c0-0.2484-0.189-0.4375-0.438-0.4375h-1.25c-0.142-0.5539-0.371-1.0512-0.656-1.5312l0.906-0.9063c0.17566-0.17566 0.17566-0.44934 0-0.625l-1.5-1.5c-0.17566-0.17566-0.44934-0.17566-0.625 0l-0.906 0.9063c-0.48-0.2844-0.977-0.5141-1.531-0.6563v-1.25c0-0.24842-0.1891-0.4375-0.4375-0.4375zm1.0625 4.1573c1.8451 0 3.3427 1.4975 3.3427 3.3427 0 1.8451-1.4975 3.3427-3.3427 3.3427-1.8451 0-3.3427-1.4979-3.3427-3.343s1.4976-3.3427 3.3427-3.3427z" display="block"/> </g> </svg> diff --git a/core/img/actions/star.png b/core/img/actions/star.png Binary files differindex 6a04282f3fa..88e4ad54584 100644 --- a/core/img/actions/star.png +++ b/core/img/actions/star.png diff --git a/core/img/actions/star.svg b/core/img/actions/star.svg index c2b3b60a2b8..36e08170a9a 100644 --- a/core/img/actions/star.svg +++ b/core/img/actions/star.svg @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> - <g transform="matrix(.068322 0 0 .068322 -10.114 -50.902)"> - <path d="m330.36 858.43 43.111 108.06 117.64 9.2572-89.445 74.392 27.55 114.75-98.391-62.079-100.62 61.66 28.637-112.76-89.734-76.638 116.09-7.6094z" transform="translate(-21.071,-112.5)" fill="#CCC"/> + <g opacity=".5" transform="matrix(.068322 0 0 .068322 -10.114 -50.902)"> + <path d="m330.36 858.43 43.111 108.06 117.64 9.2572-89.445 74.392 27.55 114.75-98.391-62.079-100.62 61.66 28.637-112.76-89.734-76.638 116.09-7.6094z" transform="translate(-21.071,-112.5)"/> </g> </svg> diff --git a/core/img/places/file.png b/core/img/places/file.png Binary files differindex c2e5db953a8..e0f04c31731 100644 --- a/core/img/places/file.png +++ b/core/img/places/file.png diff --git a/core/img/places/file.svg b/core/img/places/file.svg index be6d9866835..7db9a201a9f 100644 --- a/core/img/places/file.svg +++ b/core/img/places/file.svg @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> <rect style="color:#000000" fill-opacity="0" height="97.986" width="163.31" y="-32.993" x="-62.897"/> - <path opacity=".7" style="block-progression:tb;color:#000000;text-transform:none;text-indent:0" d="m3.3501 1.002c-0.1975 0.0382-0.3535 0.2333-0.35 0.4374v13.123c0.0000047 0.22904 0.20522 0.43743 0.43077 0.43744h10.139c0.22555-0.000006 0.43076-0.2084 0.43077-0.43744v-10.143c-0.004-0.06684-0.022-0.13284-0.054-0.19134-0.966-1.3896-2.035-2.4191-3.312-3.1988-0.043-0.0164-0.088-0.0256-0.134-0.0274h-7.0695c-0.026843-0.0026-0.053928-0.0026-0.080774 0zm5.6499 2.498c0-0.2357 0.2643-0.5 0.5-0.5h0.5v2h2v0.5c0 0.2357-0.264 0.5-0.5 0.5h-2c-0.2357 0-0.5-0.2643-0.5-0.5 0-0.46411 0.0000019-1.4917 0.0000019-2z" fill="#1e1e1e"/> + <path opacity=".5" style="block-progression:tb;color:#000000;text-transform:none;text-indent:0" d="m3.3501 1.002c-0.1975 0.0382-0.3535 0.2333-0.35 0.4374v13.123c0.0000047 0.22904 0.20522 0.43743 0.43077 0.43744h10.139c0.22555-0.000006 0.43076-0.2084 0.43077-0.43744v-10.143c-0.004-0.06684-0.022-0.13284-0.054-0.19134-0.966-1.3896-2.035-2.4191-3.312-3.1988-0.043-0.0164-0.088-0.0256-0.134-0.0274h-7.0695c-0.026843-0.0026-0.053928-0.0026-0.080774 0zm5.6499 2.498c0-0.2357 0.2643-0.5 0.5-0.5h0.5v2h2v0.5c0 0.2357-0.264 0.5-0.5 0.5h-2c-0.2357 0-0.5-0.2643-0.5-0.5 0-0.46411 0.0000019-1.4917 0.0000019-2z"/> </svg> diff --git a/core/img/places/home.png b/core/img/places/home.png Binary files differindex 5cf94495c34..2e0313d59a7 100644 --- a/core/img/places/home.png +++ b/core/img/places/home.png diff --git a/core/img/places/home.svg b/core/img/places/home.svg index 020a90f7720..2edc3af25bb 100644 --- a/core/img/places/home.svg +++ b/core/img/places/home.svg @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> <rect style="color:#000000" fill-opacity="0" height="97.986" width="163.31" y="-32.993" x="-62.897"/> - <path opacity=".7" fill="#1e1e1e" d="m8 1.0306-8 7.9694h3v6.0001h10v-6h3l-3-3.0306v-3.9695h-3v1.0812l-2-2.0505z" fill-rule="evenodd"/> + <path opacity=".5" d="m8 1.0306-8 7.9694h3v6.0001h10v-6h3l-3-3.0306v-3.9695h-3v1.0812l-2-2.0505z" fill-rule="evenodd"/> </svg> diff --git a/core/js/backgroundjobs.js b/core/js/backgroundjobs.js index 4a558a66b4b..c3100792e9e 100644 --- a/core/js/backgroundjobs.js +++ b/core/js/backgroundjobs.js @@ -22,4 +22,6 @@ // start worker once page has loaded $(document).ready(function(){ $.get( OC.webroot+'/cron.php' ); + + $('.section .icon-info').tipsy({gravity: 'w'}); }); diff --git a/core/js/config.php b/core/js/config.php index b57289fde48..d6946f671d1 100644 --- a/core/js/config.php +++ b/core/js/config.php @@ -50,14 +50,16 @@ foreach(OC_App::getEnabledApps() as $app) { $apps_paths[$app] = OC_App::getAppWebPath($app); } -$value = \OCP\Config::getAppValue('core', 'shareapi_default_expire_date', 'no'); +$config = \OC::$server->getConfig(); +$value = $config->getAppValue('core', 'shareapi_default_expire_date', 'no'); $defaultExpireDateEnabled = ($value === 'yes') ? true :false; $defaultExpireDate = $enforceDefaultExpireDate = null; if ($defaultExpireDateEnabled) { - $defaultExpireDate = (int)\OCP\Config::getAppValue('core', 'shareapi_expire_after_n_days', '7'); - $value = \OCP\Config::getAppValue('core', 'shareapi_enforce_expire_date', 'no'); + $defaultExpireDate = (int) $config->getAppValue('core', 'shareapi_expire_after_n_days', '7'); + $value = $config->getAppValue('core', 'shareapi_enforce_expire_date', 'no'); $enforceDefaultExpireDate = ($value === 'yes') ? true : false; } +$outgoingServer2serverShareEnabled = $config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes') === 'yes'; $array = array( "oc_debug" => (defined('DEBUG') && DEBUG) ? 'true' : 'false', @@ -110,6 +112,8 @@ $array = array( 'enforcePasswordForPublicLink' => \OCP\Util::isPublicLinkPasswordRequired(), 'sharingDisabledForUser' => \OCP\Util::isSharingDisabledForUser(), 'resharingAllowed' => \OCP\Share::isResharingAllowed(), + 'remoteShareAllowed' => $outgoingServer2serverShareEnabled, + 'federatedCloudShareDoc' => \OC::$server->getURLGenerator()->linkToDocs('user-sharing-federated') ) ) ), diff --git a/core/js/js.js b/core/js/js.js index 274eddffff7..cb93e73f2e0 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -476,11 +476,14 @@ var OC={ registerMenu: function($toggle, $menuEl) { $menuEl.addClass('menu'); $toggle.on('click.menu', function(event) { + // prevent the link event (append anchor to URL) + event.preventDefault(); + if ($menuEl.is(OC._currentMenu)) { $menuEl.slideUp(OC.menuSpeed); OC._currentMenu = null; OC._currentMenuToggle = null; - return false; + return; } // another menu was open? else if (OC._currentMenu) { @@ -490,7 +493,6 @@ var OC={ $menuEl.slideToggle(OC.menuSpeed); OC._currentMenu = $menuEl; OC._currentMenuToggle = $toggle; - return false; }); }, diff --git a/core/js/maintenance-check.js b/core/js/maintenance-check.js index 8ca00456fef..061a434214b 100644 --- a/core/js/maintenance-check.js +++ b/core/js/maintenance-check.js @@ -7,14 +7,14 @@ function checkStatus() { 0, location.pathname.indexOf('index.php') ); request.open("GET", ocroot+'status.php', true); - request.send(); request.onreadystatechange = function() { if (request.readyState === 4) { var response = request.responseText; var responseobj = JSON.parse(response); - if (responseobj.maintenance === 'false') { + if (responseobj.maintenance === false) { window.location.reload(); } } }; + request.send(); } diff --git a/core/js/share.js b/core/js/share.js index b9b4a5bc754..5018d10ee9c 100644 --- a/core/js/share.js +++ b/core/js/share.js @@ -391,8 +391,18 @@ OC.Share={ } }); + var sharePlaceholder = t('core', 'Share with users or groups …'); + if(oc_appconfig.core.remoteShareAllowed) { + sharePlaceholder = t('core', 'Share with users, groups or remote users …'); + } + html += '<label for="shareWith" class="hidden-visually">'+t('core', 'Share')+'</label>'; - html += '<input id="shareWith" type="text" placeholder="'+t('core', 'Share with user or group …')+'" />'; + html += '<input id="shareWith" type="text" placeholder="' + sharePlaceholder + '" />'; + if(oc_appconfig.core.remoteShareAllowed) { + var federatedCloudSharingDoc = '<a target="_blank" class="icon-info svg shareWithRemoteInfo" href="{docLink}" ' + + 'title="' + t('core', 'Share with people on other ownClouds using the syntax username@example.com/owncloud') + '"></a>'; + html += federatedCloudSharingDoc.replace('{docLink}', oc_appconfig.core.federatedCloudShareDoc); + } html += '<span class="shareWithLoading icon-loading-small hidden"></span>'; html += '<ul id="shareWithList">'; html += '</ul>'; @@ -443,6 +453,11 @@ OC.Share={ dropDownEl = $(html); dropDownEl = dropDownEl.appendTo(appendTo); + // trigger remote share info tooltip + if(oc_appconfig.core.remoteShareAllowed) { + $('.shareWithRemoteInfo').tipsy({gravity: 'e'}); + } + //Get owner avatars if (oc_config.enable_avatars === true && data !== false && data.reshare !== false && data.reshare.uid_owner !== undefined) { dropDownEl.find(".avatar").avatar(data.reshare.uid_owner, 32); diff --git a/core/register_command.php b/core/register_command.php index 67fdb6f808e..701fb10d1ba 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -26,6 +26,7 @@ /** @var $application Symfony\Component\Console\Application */ $application->add(new OC\Core\Command\Status); +$application->add(new OC\Core\Command\Check(\OC::$server->getConfig())); $application->add(new OC\Core\Command\App\CheckCode()); $application->add(new OC\Core\Command\L10n\CreateJs()); diff --git a/core/templates/installation.php b/core/templates/installation.php index 96e6119cad3..911bc05069f 100644 --- a/core/templates/installation.php +++ b/core/templates/installation.php @@ -28,7 +28,7 @@ script('core', [ <?php endif; ?> <?php if(!$_['htaccessWorking']): ?> <fieldset class="warning"> - <legend><strong><?php p($l->t('Security Warning'));?></strong></legend> + <legend><strong><?php p($l->t('Security warning'));?></strong></legend> <p><?php p($l->t('Your data directory and files are probably accessible from the internet because the .htaccess file does not work.'));?><br> <?php print_unescaped($l->t( 'For information how to properly configure your server, please see the <a href="%s" target="_blank">documentation</a>.', @@ -150,7 +150,7 @@ script('core', [ <?php if(!$_['dbIsSet'] OR count($_['errors']) > 0): ?> <fieldset id="sqliteInformation" class="warning"> - <legend><?php p($l->t('Performance Warning'));?></legend> + <legend><?php p($l->t('Performance warning'));?></legend> <p><?php p($l->t('SQLite will be used as database.'));?></p> <p><?php p($l->t('For larger installations we recommend to choose a different database backend.'));?></p> <p><?php p($l->t('Especially when using the desktop client for file syncing the use of SQLite is discouraged.')); ?></p> @@ -158,4 +158,10 @@ script('core', [ <?php endif ?> <div class="buttons"><input type="submit" class="primary" value="<?php p($l->t( 'Finish setup' )); ?>" data-finishing="<?php p($l->t( 'Finishing …' )); ?>"></div> + + <p class="info"> + <span class="icon-info-white svg"></span> + <?php p($l->t('Need help?'));?> + <a target="_blank" href="<?php p(link_to_docs('admin-install')); ?>"><?php p($l->t('See the documentation'));?> ↗</a> + </p> </form> diff --git a/core/templates/layout.user.php b/core/templates/layout.user.php index 880a276c725..87a6a9216d2 100644 --- a/core/templates/layout.user.php +++ b/core/templates/layout.user.php @@ -123,7 +123,7 @@ if(OC_User::isAdminUser(OC_User::getUser())): ?> <li id="apps-management"> - <a href="<?php print_unescaped(OC_Helper::linkToRoute('settings_apps')); ?>" tabindex="4" + <a href="<?php print_unescaped(\OC::$server->getURLGenerator()->linkToRoute('settings.AppSettings.viewApps')); ?>" tabindex="4" <?php if( $_['appsmanagement_active'] ): ?> class="active"<?php endif; ?>> <img class="app-icon svg" alt="" src="<?php print_unescaped(OC_Helper::imagePath('settings', 'apps.svg')); ?>"> <div class="icon-loading-dark" style="display:none;"></div> diff --git a/lib/base.php b/lib/base.php index be397e52449..042419eff1d 100644 --- a/lib/base.php +++ b/lib/base.php @@ -588,35 +588,36 @@ class OC { ini_set('session.cookie_secure', true); } - $errors = OC_Util::checkServer(\OC::$server->getConfig()); - if (count($errors) > 0) { - if (self::$CLI) { - // Convert l10n string into regular string for usage in database - $staticErrors = []; - foreach ($errors as $error) { - echo $error['error'] . "\n"; - echo $error['hint'] . "\n\n"; - $staticErrors[] = [ - 'error' => (string) $error['error'], - 'hint' => (string) $error['hint'], - ]; - } + if (!defined('OC_CONSOLE')) { + $errors = OC_Util::checkServer(\OC::$server->getConfig()); + if (count($errors) > 0) { + if (self::$CLI) { + // Convert l10n string into regular string for usage in database + $staticErrors = []; + foreach ($errors as $error) { + echo $error['error'] . "\n"; + echo $error['hint'] . "\n\n"; + $staticErrors[] = [ + 'error' => (string)$error['error'], + 'hint' => (string)$error['hint'], + ]; + } - try { - \OC::$server->getConfig()->setAppValue('core', 'cronErrors', json_encode($staticErrors)); - } catch(\Exception $e) { - echo('Writing to database failed'); + try { + \OC::$server->getConfig()->setAppValue('core', 'cronErrors', json_encode($staticErrors)); + } catch (\Exception $e) { + echo('Writing to database failed'); + } + exit(1); + } else { + OC_Response::setStatus(OC_Response::STATUS_SERVICE_UNAVAILABLE); + OC_Template::printGuestPage('', 'error', array('errors' => $errors)); + exit; } - exit(1); - } else { - OC_Response::setStatus(OC_Response::STATUS_SERVICE_UNAVAILABLE); - OC_Template::printGuestPage('', 'error', array('errors' => $errors)); - exit; - } - } elseif(self::$CLI && \OC::$server->getConfig()->getSystemValue('installed', false)) { + } elseif (self::$CLI && \OC::$server->getConfig()->getSystemValue('installed', false)) { \OC::$server->getConfig()->deleteAppValue('core', 'cronErrors'); + } } - //try to set the session lifetime $sessionLifeTime = self::getSessionLifeTime(); @ini_set('gc_maxlifetime', (string)$sessionLifeTime); diff --git a/lib/private/activitymanager.php b/lib/private/activitymanager.php index c6cd5a1fe83..26db0c78df2 100644 --- a/lib/private/activitymanager.php +++ b/lib/private/activitymanager.php @@ -28,8 +28,34 @@ namespace OC; use OCP\Activity\IConsumer; use OCP\Activity\IExtension; use OCP\Activity\IManager; +use OCP\IConfig; +use OCP\IRequest; +use OCP\IUserSession; class ActivityManager implements IManager { + /** @var IRequest */ + protected $request; + + /** @var IUserSession */ + protected $session; + + /** @var IConfig */ + protected $config; + + /** + * constructor of the controller + * + * @param IRequest $request + * @param IUserSession $session + * @param IConfig $config + */ + public function __construct(IRequest $request, + IUserSession $session, + IConfig $config) { + $this->request = $request; + $this->session = $session; + $this->config = $config; + } /** * @var \Closure[] @@ -348,4 +374,43 @@ class ActivityManager implements IManager { return array(' and ((' . implode(') or (', $conditions) . '))', $parameters); } + + /** + * Get the user we need to use + * + * Either the user is logged in, or we try to get it from the token + * + * @return string + * @throws \UnexpectedValueException If the token is invalid, does not exist or is not unique + */ + public function getCurrentUserId() { + if (!$this->session->isLoggedIn()) { + return $this->getUserFromToken(); + } else { + return $this->session->getUser()->getUID(); + } + } + + /** + * Get the user for the token + * + * @return string + * @throws \UnexpectedValueException If the token is invalid, does not exist or is not unique + */ + protected function getUserFromToken() { + $token = (string) $this->request->getParam('token', ''); + if (strlen($token) !== 30) { + throw new \UnexpectedValueException('The token is invalid'); + } + + $users = $this->config->getUsersForUserValue('activity', 'rsstoken', $token); + + if (sizeof($users) !== 1) { + // No unique user found + throw new \UnexpectedValueException('The token is invalid'); + } + + // Token found login as that user + return array_shift($users); + } } diff --git a/lib/private/allconfig.php b/lib/private/allconfig.php index df75a332a13..63cc92601bb 100644 --- a/lib/private/allconfig.php +++ b/lib/private/allconfig.php @@ -253,7 +253,7 @@ class AllConfig implements \OCP\IConfig { * @param string $userId the userId of the user that we want to store the value under * @param string $appName the appName that we stored the value under * @param string $key the key under which the value is being stored - * @param string $default the default value to be returned if the value isn't set + * @param mixed $default the default value to be returned if the value isn't set * @return string */ public function getUserValue($userId, $appName, $key, $default = '') { diff --git a/lib/private/app.php b/lib/private/app.php index 4b3d4b82b82..aec67e6efd6 100644 --- a/lib/private/app.php +++ b/lib/private/app.php @@ -61,6 +61,7 @@ class OC_App { static private $loadedApps = array(); static private $altLogin = array(); private static $shippedApps = null; + const officialApp = 200; /** * clean the appId @@ -306,8 +307,13 @@ class OC_App { * @return int */ public static function downloadApp($app) { - $appData= OCSClient::getApplication($app); - $download= OCSClient::getApplicationDownload($app, 1); + $ocsClient = new OCSClient( + \OC::$server->getHTTPClientService(), + \OC::$server->getConfig(), + \OC::$server->getLogger() + ); + $appData = $ocsClient->getApplication($app); + $download= $ocsClient->getApplicationDownload($app); if(isset($download['downloadlink']) and $download['downloadlink']!='') { // Replace spaces in download link without encoding entire URL $download['downloadlink'] = str_replace(' ', '%20', $download['downloadlink']); @@ -507,7 +513,7 @@ class OC_App { /** * search for an app in all app-directories * - * @param $appId + * @param string $appId * @return mixed (bool|string) */ protected static function findAppInDirectories($appId) { @@ -726,6 +732,8 @@ class OC_App { /** * register a personal form to be shown + * @param string $app + * @param string $page */ public static function registerPersonal($app, $page) { self::$personalForms[] = $app . '/' . $page . '.php'; @@ -780,8 +788,9 @@ class OC_App { } /** - * Lists all apps, this is used in apps.php + * List all apps, this is used in apps.php * + * @param bool $onlyLocal * @return array */ public static function listAllApps($onlyLocal = false) { @@ -819,8 +828,7 @@ class OC_App { if (isset($info['shipped']) and ($info['shipped'] == 'true')) { $info['internal'] = true; - $info['internallabel'] = (string)$l->t('Recommended'); - $info['internalclass'] = 'recommendedapp'; + $info['level'] = self::officialApp; $info['removable'] = false; } else { $info['internal'] = false; @@ -845,7 +853,7 @@ class OC_App { } } if ($onlyLocal) { - $remoteApps = array(); + $remoteApps = []; } else { $remoteApps = OC_App::getAppstoreApps(); } @@ -865,34 +873,6 @@ class OC_App { } else { $combinedApps = $appList; } - // bring the apps into the right order with a custom sort function - usort($combinedApps, function ($a, $b) { - - // priority 1: active - if ($a['active'] != $b['active']) { - return $b['active'] - $a['active']; - } - - // priority 2: shipped - $aShipped = (array_key_exists('shipped', $a) && $a['shipped'] === 'true') ? 1 : 0; - $bShipped = (array_key_exists('shipped', $b) && $b['shipped'] === 'true') ? 1 : 0; - if ($aShipped !== $bShipped) { - return ($bShipped - $aShipped); - } - - // priority 3: recommended - $internalClassA = isset($a['internalclass']) ? $a['internalclass'] : ''; - $internalClassB = isset($b['internalclass']) ? $b['internalclass'] : ''; - if ($internalClassA != $internalClassB) { - $aTemp = ($internalClassA == 'recommendedapp' ? 1 : 0); - $bTemp = ($internalClassB == 'recommendedapp' ? 1 : 0); - return ($bTemp - $aTemp); - } - - // priority 4: alphabetical - return strcasecmp($a['name'], $b['name']); - - }); return $combinedApps; } @@ -913,15 +893,24 @@ class OC_App { } /** - * get a list of all apps on apps.owncloud.com - * - * @return array|false multi-dimensional array of apps. - * Keys: id, name, type, typename, personid, license, detailpage, preview, changed, description + * Get a list of all apps on the appstore + * @param string $filter + * @param string $category + * @return array|bool multi-dimensional array of apps. + * Keys: id, name, type, typename, personid, license, detailpage, preview, changed, description */ public static function getAppstoreApps($filter = 'approved', $category = null) { - $categories = array($category); + $categories = [$category]; + + $ocsClient = new OCSClient( + \OC::$server->getHTTPClientService(), + \OC::$server->getConfig(), + \OC::$server->getLogger() + ); + + if (is_null($category)) { - $categoryNames = OCSClient::getCategories(); + $categoryNames = $ocsClient->getCategories(); if (is_array($categoryNames)) { // Check that categories of apps were retrieved correctly if (!$categories = array_keys($categoryNames)) { @@ -933,34 +922,36 @@ class OC_App { } $page = 0; - $remoteApps = OCSClient::getApplications($categories, $page, $filter); - $app1 = array(); + $remoteApps = $ocsClient->getApplications($categories, $page, $filter); + $apps = []; $i = 0; $l = \OC::$server->getL10N('core'); foreach ($remoteApps as $app) { $potentialCleanId = self::getInternalAppIdByOcs($app['id']); // enhance app info (for example the description) - $app1[$i] = OC_App::parseAppInfo($app); - $app1[$i]['author'] = $app['personid']; - $app1[$i]['ocs_id'] = $app['id']; - $app1[$i]['internal'] = 0; - $app1[$i]['active'] = ($potentialCleanId !== false) ? self::isEnabled($potentialCleanId) : false; - $app1[$i]['update'] = false; - $app1[$i]['groups'] = false; - $app1[$i]['score'] = $app['score']; - $app1[$i]['removable'] = false; + $apps[$i] = OC_App::parseAppInfo($app); + $apps[$i]['author'] = $app['personid']; + $apps[$i]['ocs_id'] = $app['id']; + $apps[$i]['internal'] = 0; + $apps[$i]['active'] = ($potentialCleanId !== false) ? self::isEnabled($potentialCleanId) : false; + $apps[$i]['update'] = false; + $apps[$i]['groups'] = false; + $apps[$i]['score'] = $app['score']; + $apps[$i]['removable'] = false; if ($app['label'] == 'recommended') { - $app1[$i]['internallabel'] = (string)$l->t('Recommended'); - $app1[$i]['internalclass'] = 'recommendedapp'; + $apps[$i]['internallabel'] = (string)$l->t('Recommended'); + $apps[$i]['internalclass'] = 'recommendedapp'; } $i++; } - if (empty($app1)) { + + + if (empty($apps)) { return false; } else { - return $app1; + return $apps; } } @@ -1084,7 +1075,12 @@ class OC_App { public static function installApp($app) { $l = \OC::$server->getL10N('core'); $config = \OC::$server->getConfig(); - $appData=OCSClient::getApplication($app); + $ocsClient = new OCSClient( + \OC::$server->getHTTPClientService(), + $config, + \OC::$server->getLogger() + ); + $appData = $ocsClient->getApplication($app); // check if app is a shipped app or not. OCS apps have an integer as id, shipped apps use a string if (!is_numeric($app)) { diff --git a/lib/private/app/appmanager.php b/lib/private/app/appmanager.php index 2a147d4de6f..c9d4a777c4a 100644 --- a/lib/private/app/appmanager.php +++ b/lib/private/app/appmanager.php @@ -203,7 +203,7 @@ class AppManager implements IAppManager { /** * Clear the cached list of apps when enabling/disabling an app */ - protected function clearAppsCache() { + public function clearAppsCache() { $settingsMemCache = $this->memCacheFactory->create('settings'); $settingsMemCache->clear('listApps'); } diff --git a/lib/private/encryption/exceptions/decryptionfailedexception.php b/lib/private/encryption/exceptions/decryptionfailedexception.php index f8b4fdf07fa..406ae12968e 100644 --- a/lib/private/encryption/exceptions/decryptionfailedexception.php +++ b/lib/private/encryption/exceptions/decryptionfailedexception.php @@ -1,7 +1,8 @@ <?php - /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 2/25/15, 9:38 AM +/** + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * diff --git a/lib/private/encryption/exceptions/emptyencryptiondataexception.php b/lib/private/encryption/exceptions/emptyencryptiondataexception.php index d3dc9230047..739614b3ec2 100644 --- a/lib/private/encryption/exceptions/emptyencryptiondataexception.php +++ b/lib/private/encryption/exceptions/emptyencryptiondataexception.php @@ -1,7 +1,8 @@ <?php - /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 2/25/15, 9:38 AM +/** + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * diff --git a/lib/private/encryption/exceptions/encryptionfailedexception.php b/lib/private/encryption/exceptions/encryptionfailedexception.php index ac489c73254..4195ca0a5a8 100644 --- a/lib/private/encryption/exceptions/encryptionfailedexception.php +++ b/lib/private/encryption/exceptions/encryptionfailedexception.php @@ -1,7 +1,8 @@ <?php - /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 2/25/15, 9:37 AM +/** + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * diff --git a/lib/private/encryption/exceptions/encryptionheaderkeyexistsexception.php b/lib/private/encryption/exceptions/encryptionheaderkeyexistsexception.php index 5e8e48efd78..d927939484f 100644 --- a/lib/private/encryption/exceptions/encryptionheaderkeyexistsexception.php +++ b/lib/private/encryption/exceptions/encryptionheaderkeyexistsexception.php @@ -1,24 +1,23 @@ <?php - /** - * ownCloud - * - * @copyright (C) 2015 ownCloud, Inc. + * @author Björn Schießle <schiessle@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> * - * @author Bjoern Schiessle <schiessle@owncloud.com> + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. + * 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 library is distributed in the hope that it will be useful, + * 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. + * 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/> * - * You should have received a copy of the GNU Affero General Public - * License along with this library. If not, see <http://www.gnu.org/licenses/>. */ namespace OC\Encryption\Exceptions; diff --git a/lib/private/encryption/exceptions/encryptionheadertolargeexception.php b/lib/private/encryption/exceptions/encryptionheadertolargeexception.php index cdb5f940800..40c51782a32 100644 --- a/lib/private/encryption/exceptions/encryptionheadertolargeexception.php +++ b/lib/private/encryption/exceptions/encryptionheadertolargeexception.php @@ -1,7 +1,8 @@ <?php - /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 2/25/15, 9:35 AM +/** + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * diff --git a/lib/private/encryption/exceptions/modulealreadyexistsexception.php b/lib/private/encryption/exceptions/modulealreadyexistsexception.php index fa1e70a5c36..c72ad7b7ab2 100644 --- a/lib/private/encryption/exceptions/modulealreadyexistsexception.php +++ b/lib/private/encryption/exceptions/modulealreadyexistsexception.php @@ -1,24 +1,23 @@ <?php - /** - * ownCloud - * - * @copyright (C) 2015 ownCloud, Inc. + * @author Björn Schießle <schiessle@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> * - * @author Bjoern Schiessle <schiessle@owncloud.com> + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. + * 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 library is distributed in the hope that it will be useful, + * 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. + * 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/> * - * You should have received a copy of the GNU Affero General Public - * License along with this library. If not, see <http://www.gnu.org/licenses/>. */ namespace OC\Encryption\Exceptions; diff --git a/lib/private/encryption/exceptions/moduledoesnotexistsexception.php b/lib/private/encryption/exceptions/moduledoesnotexistsexception.php index 2c699e8dc2d..d6fbb2b6e51 100644 --- a/lib/private/encryption/exceptions/moduledoesnotexistsexception.php +++ b/lib/private/encryption/exceptions/moduledoesnotexistsexception.php @@ -1,24 +1,23 @@ <?php - /** - * ownCloud - * - * @copyright (C) 2015 ownCloud, Inc. + * @author Björn Schießle <schiessle@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> * - * @author Bjoern Schiessle <schiessle@owncloud.com> + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. + * 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 library is distributed in the hope that it will be useful, + * 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. + * 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/> * - * You should have received a copy of the GNU Affero General Public - * License along with this library. If not, see <http://www.gnu.org/licenses/>. */ namespace OC\Encryption\Exceptions; diff --git a/lib/private/encryption/exceptions/unknowncipherexception.php b/lib/private/encryption/exceptions/unknowncipherexception.php index 188f7403848..91535183169 100644 --- a/lib/private/encryption/exceptions/unknowncipherexception.php +++ b/lib/private/encryption/exceptions/unknowncipherexception.php @@ -1,7 +1,8 @@ <?php - /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 2/25/15, 9:36 AM +/** + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * diff --git a/lib/private/encryption/file.php b/lib/private/encryption/file.php index 3600936ed0e..48cd0d1187b 100644 --- a/lib/private/encryption/file.php +++ b/lib/private/encryption/file.php @@ -1,24 +1,23 @@ <?php - /** - * ownCloud - * - * @copyright (C) 2015 ownCloud, Inc. + * @author Björn Schießle <schiessle@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> * - * @author Bjoern Schiessle <schiessle@owncloud.com> + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. + * 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 library is distributed in the hope that it will be useful, + * 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. + * 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/> * - * You should have received a copy of the GNU Affero General Public - * License along with this library. If not, see <http://www.gnu.org/licenses/>. */ namespace OC\Encryption; diff --git a/lib/private/encryption/keys/factory.php b/lib/private/encryption/keys/factory.php index a214b238615..0e2b0292a68 100644 --- a/lib/private/encryption/keys/factory.php +++ b/lib/private/encryption/keys/factory.php @@ -1,24 +1,22 @@ <?php - /** - * ownCloud - * - * @copyright (C) 2015 ownCloud, Inc. + * @author Björn Schießle <schiessle@owncloud.com> * - * @author Bjoern Schiessle <schiessle@owncloud.com> + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. + * 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 library is distributed in the hope that it will be useful, + * 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. + * 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/> * - * You should have received a copy of the GNU Affero General Public - * License along with this library. If not, see <http://www.gnu.org/licenses/>. */ namespace OC\Encryption\Keys; diff --git a/lib/private/encryption/keys/storage.php b/lib/private/encryption/keys/storage.php index 42610bd0b41..9d978193130 100644 --- a/lib/private/encryption/keys/storage.php +++ b/lib/private/encryption/keys/storage.php @@ -1,24 +1,23 @@ <?php - /** - * ownCloud - * - * @copyright (C) 2015 ownCloud, Inc. + * @author Björn Schießle <schiessle@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> * - * @author Bjoern Schiessle <schiessle@owncloud.com> + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. + * 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 library is distributed in the hope that it will be useful, + * 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. + * 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/> * - * You should have received a copy of the GNU Affero General Public - * License along with this library. If not, see <http://www.gnu.org/licenses/>. */ namespace OC\Encryption\Keys; @@ -141,11 +140,11 @@ class Storage implements \OCP\Encryption\Keys\IStorage { * @param string $uid ID if the user for whom we want to delete the key * @param string $keyId id of the key * - * @return boolean + * @return boolean False when the key could not be deleted */ public function deleteUserKey($uid, $keyId) { $path = $this->constructUserKeyPath($keyId, $uid); - return $this->view->unlink($path); + return !$this->view->file_exists($path) || $this->view->unlink($path); } /** @@ -154,22 +153,23 @@ class Storage implements \OCP\Encryption\Keys\IStorage { * @param string $path path to file * @param string $keyId id of the key * - * @return boolean + * @return boolean False when the key could not be deleted */ public function deleteFileKey($path, $keyId) { $keyDir = $this->getFileKeyDir($path); - return $this->view->unlink($keyDir . $keyId); + return !$this->view->file_exists($keyDir . $keyId) || $this->view->unlink($keyDir . $keyId); } /** * delete all file keys for a given file * * @param string $path to the file - * @return boolean + * @return boolean False when the key could not be deleted */ public function deleteAllFileKeys($path) { $keyDir = $this->getFileKeyDir($path); - return $this->view->deleteAll(dirname($keyDir)); + $path = dirname($keyDir); + return !$this->view->file_exists($path) || $this->view->deleteAll($path); } /** @@ -178,11 +178,11 @@ class Storage implements \OCP\Encryption\Keys\IStorage { * * @param string $keyId id of the key * - * @return boolean + * @return boolean False when the key could not be deleted */ public function deleteSystemUserKey($keyId) { $path = $this->constructUserKeyPath($keyId); - return $this->view->unlink($path); + return !$this->view->file_exists($path) || $this->view->unlink($path); } diff --git a/lib/private/encryption/manager.php b/lib/private/encryption/manager.php index 484e0f540b2..74cad0a75bb 100644 --- a/lib/private/encryption/manager.php +++ b/lib/private/encryption/manager.php @@ -1,24 +1,23 @@ <?php - /** - * ownCloud - * - * @copyright (C) 2015 ownCloud, Inc. + * @author Björn Schießle <schiessle@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> * - * @author Bjoern Schiessle <schiessle@owncloud.com> + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. + * 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 library is distributed in the hope that it will be useful, + * 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. + * 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/> * - * You should have received a copy of the GNU Affero General Public - * License along with this library. If not, see <http://www.gnu.org/licenses/>. */ namespace OC\Encryption; diff --git a/lib/private/encryption/update.php b/lib/private/encryption/update.php index 1cfe935e584..7a170a03adc 100644 --- a/lib/private/encryption/update.php +++ b/lib/private/encryption/update.php @@ -1,24 +1,23 @@ <?php - /** - * ownCloud - * - * @copyright (C) 2015 ownCloud, Inc. + * @author Björn Schießle <schiessle@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> * - * @author Bjoern Schiessle <schiessle@owncloud.com> + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. + * 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 library is distributed in the hope that it will be useful, + * 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. + * 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/> * - * You should have received a copy of the GNU Affero General Public - * License along with this library. If not, see <http://www.gnu.org/licenses/>. */ namespace OC\Encryption; diff --git a/lib/private/encryption/util.php b/lib/private/encryption/util.php index 6312d8813e3..e7cf607c7b1 100644 --- a/lib/private/encryption/util.php +++ b/lib/private/encryption/util.php @@ -1,24 +1,23 @@ <?php - /** - * ownCloud - * - * @copyright (C) 2015 ownCloud, Inc. + * @author Björn Schießle <schiessle@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> * - * @author Bjoern Schiessle <schiessle@owncloud.com> + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. + * 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 library is distributed in the hope that it will be useful, + * 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. + * 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/> * - * You should have received a copy of the GNU Affero General Public - * License along with this library. If not, see <http://www.gnu.org/licenses/>. */ namespace OC\Encryption; diff --git a/lib/private/files/storage/dav.php b/lib/private/files/storage/dav.php index 629f68e60f0..9f779208a09 100644 --- a/lib/private/files/storage/dav.php +++ b/lib/private/files/storage/dav.php @@ -58,6 +58,11 @@ class DAV extends \OC\Files\Storage\Common { */ private $client; + /** + * @var \OC\MemCache\ArrayCache + */ + private $statCache; + /** @var array */ private static $tempFiles = []; @@ -66,6 +71,7 @@ class DAV extends \OC\Files\Storage\Common { * @throws \Exception */ public function __construct($params) { + $this->statCache = new \OC\MemCache\ArrayCache(); if (isset($params['host']) && isset($params['user']) && isset($params['password'])) { $host = $params['host']; //remove leading http[s], will be generated in createBaseUri() @@ -121,6 +127,13 @@ class DAV extends \OC\Files\Storage\Common { } } + /** + * Clear the stat cache + */ + public function clearStatCache() { + $this->statCache->clear(); + } + /** {@inheritdoc} */ public function getId() { return 'webdav::' . $this->user . '@' . $this->host . '/' . $this->root; @@ -140,16 +153,23 @@ class DAV extends \OC\Files\Storage\Common { public function mkdir($path) { $this->init(); $path = $this->cleanPath($path); - return $this->simpleResponse('MKCOL', $path, null, 201); + $result = $this->simpleResponse('MKCOL', $path, null, 201); + if ($result) { + $this->statCache->set($path, true); + } + return $result; } /** {@inheritdoc} */ public function rmdir($path) { $this->init(); - $path = $this->cleanPath($path) . '/'; + $path = $this->cleanPath($path); // FIXME: some WebDAV impl return 403 when trying to DELETE // a non-empty folder - return $this->simpleResponse('DELETE', $path, null, 204); + $result = $this->simpleResponse('DELETE', $path . '/', null, 204); + $this->statCache->clear($path . '/'); + $this->statCache->remove($path); + return $result; } /** {@inheritdoc} */ @@ -157,19 +177,34 @@ class DAV extends \OC\Files\Storage\Common { $this->init(); $path = $this->cleanPath($path); try { - $response = $this->client->propfind($this->encodePath($path), array(), 1); + $response = $this->client->propfind( + $this->encodePath($path), + array(), + 1 + ); $id = md5('webdav' . $this->root . $path); $content = array(); $files = array_keys($response); array_shift($files); //the first entry is the current directory + + if (!$this->statCache->hasKey($path)) { + $this->statCache->set($path, true); + } foreach ($files as $file) { - $file = urldecode(basename($file)); + $file = urldecode($file); + // do not store the real entry, we might not have all properties + if (!$this->statCache->hasKey($path)) { + $this->statCache->set($file, true); + } + $file = basename($file); $content[] = $file; } \OC\Files\Stream\Dir::register($id, $content); return opendir('fakedir://' . $id); } catch (ClientHttpException $e) { if ($e->getHttpStatus() === 404) { + $this->statCache->clear($path . '/'); + $this->statCache->set($path, false); return false; } $this->convertSabreException($e); @@ -181,12 +216,57 @@ class DAV extends \OC\Files\Storage\Common { } } + /** + * Propfind call with cache handling. + * + * First checks if information is cached. + * If not, request it from the server then store to cache. + * + * @param string $path path to propfind + * + * @return array propfind response + * + * @throws Exception\NotFound + */ + private function propfind($path) { + $path = $this->cleanPath($path); + $cachedResponse = $this->statCache->get($path); + if ($cachedResponse === false) { + // we know it didn't exist + throw new Exception\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( + $this->encodePath($path), + array( + '{DAV:}getlastmodified', + '{DAV:}getcontentlength', + '{DAV:}getcontenttype', + '{http://owncloud.org/ns}permissions', + '{DAV:}resourcetype', + '{DAV:}getetag', + ) + ); + $this->statCache->set($path, $response); + } catch (Exception\NotFound $e) { + // remember that this path did not exist + $this->statCache->clear($path . '/'); + $this->statCache->set($path, false); + throw $e; + } + } else { + $response = $cachedResponse; + } + return $response; + } + /** {@inheritdoc} */ public function filetype($path) { - $this->init(); - $path = $this->cleanPath($path); try { - $response = $this->client->propfind($this->encodePath($path), array('{DAV:}resourcetype')); + $response = $this->propfind($path); $responseType = array(); if (isset($response["{DAV:}resourcetype"])) { $responseType = $response["{DAV:}resourcetype"]->resourceType; @@ -207,10 +287,17 @@ class DAV extends \OC\Files\Storage\Common { /** {@inheritdoc} */ public function file_exists($path) { - $this->init(); - $path = $this->cleanPath($path); try { - $this->client->propfind($this->encodePath($path), array('{DAV:}resourcetype')); + $path = $this->cleanPath($path); + $cachedState = $this->statCache->get($path); + if ($cachedState === false) { + // we know the file doesn't exist + return false; + } else if (!is_null($cachedState)) { + return true; + } + // need to get from server + $this->propfind($path); return true; //no 404 exception } catch (ClientHttpException $e) { if ($e->getHttpStatus() === 404) { @@ -228,7 +315,11 @@ class DAV extends \OC\Files\Storage\Common { /** {@inheritdoc} */ public function unlink($path) { $this->init(); - return $this->simpleResponse('DELETE', $path, null, 204); + $path = $this->cleanPath($path); + $result = $this->simpleResponse('DELETE', $path, null, 204); + $this->statCache->clear($path . '/'); + $this->statCache->remove($path); + return $result; } /** {@inheritdoc} */ @@ -316,6 +407,7 @@ class DAV extends \OC\Files\Storage\Common { $this->init(); $path = $this->cleanPath($path); try { + // TODO: cacheable ? $response = $this->client->propfind($this->encodePath($path), array('{DAV:}quota-available-bytes')); if (isset($response['{DAV:}quota-available-bytes'])) { return (int)$response['{DAV:}quota-available-bytes']; @@ -338,6 +430,7 @@ class DAV extends \OC\Files\Storage\Common { // if file exists, update the mtime, else create a new empty file if ($this->file_exists($path)) { try { + $this->statCache->remove($path); $this->client->proppatch($this->encodePath($path), array('{DAV:}lastmodified' => $mtime)); } catch (ClientHttpException $e) { if ($e->getHttpStatus() === 501) { @@ -358,10 +451,24 @@ class DAV extends \OC\Files\Storage\Common { /** * @param string $path + * @param string $data + */ + public function file_put_contents($path, $data) { + $path = $this->cleanPath($path); + $result = parent::file_put_contents($path, $data); + $this->statCache->remove($path); + return $result; + } + + /** + * @param string $path * @param string $target */ protected function uploadFile($path, $target) { $this->init(); + // invalidate + $target = $this->cleanPath($target); + $this->statCache->remove($target); $source = fopen($path, 'r'); $curl = curl_init(); @@ -394,10 +501,21 @@ class DAV extends \OC\Files\Storage\Common { /** {@inheritdoc} */ public function rename($path1, $path2) { $this->init(); - $path1 = $this->encodePath($this->cleanPath($path1)); - $path2 = $this->createBaseUri() . $this->encodePath($this->cleanPath($path2)); + $path1 = $this->cleanPath($path1); + $path2 = $this->cleanPath($path2); try { - $this->client->request('MOVE', $path1, null, array('Destination' => $path2)); + $this->client->request( + 'MOVE', + $this->encodePath($path1), + null, + array( + 'Destination' => $this->createBaseUri() . $this->encodePath($path2) + ) + ); + $this->statCache->clear($path1 . '/'); + $this->statCache->clear($path2 . '/'); + $this->statCache->set($path1, false); + $this->statCache->set($path2, true); $this->removeCachedFile($path1); $this->removeCachedFile($path2); return true; @@ -418,6 +536,8 @@ class DAV extends \OC\Files\Storage\Common { $path2 = $this->createBaseUri() . $this->encodePath($this->cleanPath($path2)); try { $this->client->request('COPY', $path1, null, array('Destination' => $path2)); + $this->statCache->clear($path2 . '/'); + $this->statCache->set($path2, true); $this->removeCachedFile($path2); return true; } catch (ClientHttpException $e) { @@ -432,10 +552,8 @@ class DAV extends \OC\Files\Storage\Common { /** {@inheritdoc} */ public function stat($path) { - $this->init(); - $path = $this->cleanPath($path); try { - $response = $this->client->propfind($this->encodePath($path), array('{DAV:}getlastmodified', '{DAV:}getcontentlength')); + $response = $this->propfind($path); return array( 'mtime' => strtotime($response['{DAV:}getlastmodified']), 'size' => (int)isset($response['{DAV:}getcontentlength']) ? $response['{DAV:}getcontentlength'] : 0, @@ -455,10 +573,8 @@ class DAV extends \OC\Files\Storage\Common { /** {@inheritdoc} */ public function getMimeType($path) { - $this->init(); - $path = $this->cleanPath($path); try { - $response = $this->client->propfind($this->encodePath($path), array('{DAV:}getcontenttype', '{DAV:}resourcetype')); + $response = $this->propfind($path); $responseType = array(); if (isset($response["{DAV:}resourcetype"])) { $responseType = $response["{DAV:}resourcetype"]->resourceType; @@ -489,7 +605,7 @@ class DAV extends \OC\Files\Storage\Common { * @return string */ public function cleanPath($path) { - if ($path === "") { + if ($path === '') { return $path; } $path = \OC\Files\Filesystem::normalizePath($path); @@ -524,6 +640,8 @@ class DAV extends \OC\Files\Storage\Common { return $response['statusCode'] == $expected; } catch (ClientHttpException $e) { if ($e->getHttpStatus() === 404 && $method === 'DELETE') { + $this->statCache->clear($path . '/'); + $this->statCache->set($path, false); return false; } @@ -613,11 +731,9 @@ class DAV extends \OC\Files\Storage\Common { $this->init(); $path = $this->cleanPath($path); try { - $response = $this->client->propfind($this->encodePath($path), array( - '{DAV:}getlastmodified', - '{DAV:}getetag', - '{http://owncloud.org/ns}permissions' - )); + // force refresh for $path + $this->statCache->remove($path); + $response = $this->propfind($path); if (isset($response['{DAV:}getetag'])) { $cachedData = $this->getCache()->get($path); $etag = trim($response['{DAV:}getetag'], '"'); diff --git a/lib/private/files/storage/local.php b/lib/private/files/storage/local.php index cf82e218969..6bd9b4401d6 100644 --- a/lib/private/files/storage/local.php +++ b/lib/private/files/storage/local.php @@ -228,6 +228,19 @@ if (\OC_Util::runningOnWindows()) { $this->unlink($path2); } + if ($this->is_dir($path1)) { + // we cant move folders across devices, use copy instead + $stat1 = stat(dirname($this->getSourcePath($path1))); + $stat2 = stat(dirname($this->getSourcePath($path2))); + if ($stat1['dev'] !== $stat2['dev']) { + $result = $this->copy($path1, $path2); + if ($result) { + $result &= $this->rmdir($path1); + } + return $result; + } + } + return rename($this->getSourcePath($path1), $this->getSourcePath($path2)); } diff --git a/lib/private/files/storage/wrapper/encryption.php b/lib/private/files/storage/wrapper/encryption.php index 5245fe4cc45..4136e008af9 100644 --- a/lib/private/files/storage/wrapper/encryption.php +++ b/lib/private/files/storage/wrapper/encryption.php @@ -1,24 +1,23 @@ <?php - /** - * ownCloud - * - * @copyright (C) 2015 ownCloud, Inc. + * @author Björn Schießle <schiessle@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> * - * @author Bjoern Schiessle <schiessle@owncloud.com> + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. + * 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 library is distributed in the hope that it will be useful, + * 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. + * 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/> * - * You should have received a copy of the GNU Affero General Public - * License along with this library. If not, see <http://www.gnu.org/licenses/>. */ namespace OC\Files\Storage\Wrapper; @@ -227,6 +226,7 @@ class Encryption extends Wrapper { */ public function fopen($path, $mode) { + $encryptionEnabled = $this->encryptionManager->isEnabled(); $shouldEncrypt = false; $encryptionModule = null; $header = $this->getHeader($path); @@ -259,10 +259,11 @@ class Encryption extends Wrapper { ) { if (!empty($encryptionModuleId)) { $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId); - } else { + $shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath); + } elseif ($encryptionEnabled) { $encryptionModule = $this->encryptionManager->getDefaultEncryptionModule(); + $shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath); } - $shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath); } else { // only get encryption module if we found one in the header if (!empty($encryptionModuleId)) { @@ -272,12 +273,11 @@ class Encryption extends Wrapper { } } catch (ModuleDoesNotExistsException $e) { $this->logger->warning('Encryption module "' . $encryptionModuleId . - '" not found, file will be stored unencrypted'); + '" not found, file will be stored unencrypted (' . $e->getMessage() . ')'); } // encryption disabled on write of new file and write to existing unencrypted file -> don't encrypt - $encEnabled = $this->encryptionManager->isEnabled(); - if (!$encEnabled || !$this->mount->getOption('encrypt', true)) { + if (!$encryptionEnabled || !$this->mount->getOption('encrypt', true)) { if (!$targetExists || !$targetIsEncrypted) { $shouldEncrypt = false; } diff --git a/lib/private/files/stream/encryption.php b/lib/private/files/stream/encryption.php index b4e06c99943..9ba98e61d3e 100644 --- a/lib/private/files/stream/encryption.php +++ b/lib/private/files/stream/encryption.php @@ -1,24 +1,24 @@ <?php - /** - * ownCloud - Encryption stream wrapper - * - * @copyright (C) 2015 ownCloud, Inc. + * @author Björn Schießle <schiessle@owncloud.com> + * @author jknockaert <jasper@knockaert.nl> + * @author Thomas Müller <thomas.mueller@tmit.eu> * - * @author Bjoern Schiessle <schiessle@owncloud.com> + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. + * 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 library is distributed in the hope that it will be useful, + * 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. + * 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/> * - * You should have received a copy of the GNU Affero General Public - * License along with this library. If not, see <http://www.gnu.org/licenses/>. */ namespace OC\Files\Stream; diff --git a/lib/private/files/view.php b/lib/private/files/view.php index 0f371bbc5ea..ab7a7d3db9a 100644 --- a/lib/private/files/view.php +++ b/lib/private/files/view.php @@ -762,7 +762,9 @@ class View { if (is_resource($dh)) { while (($file = readdir($dh)) !== false) { if (!Filesystem::isIgnoredDir($file)) { - $result = $this->copy($path1 . '/' . $file, $path2 . '/' . $file, $preserveMtime); + if (!$this->copy($path1 . '/' . $file, $path2 . '/' . $file, $preserveMtime)) { + $result = false; + } } } } diff --git a/lib/private/installer.php b/lib/private/installer.php index e30344b1b10..41f13f0f5f9 100644 --- a/lib/private/installer.php +++ b/lib/private/installer.php @@ -222,8 +222,13 @@ class OC_Installer{ * @throws Exception */ public static function updateAppByOCSId($ocsId) { - $appData = OCSClient::getApplication($ocsId); - $download = OCSClient::getApplicationDownload($ocsId, 1); + $ocsClient = new OCSClient( + \OC::$server->getHTTPClientService(), + \OC::$server->getConfig(), + \OC::$server->getLogger() + ); + $appData = $ocsClient->getApplication($ocsId); + $download = $ocsClient->getApplicationDownload($ocsId); if (isset($download['downloadlink']) && trim($download['downloadlink']) !== '') { $download['downloadlink'] = str_replace(' ', '%20', $download['downloadlink']); @@ -385,8 +390,12 @@ class OC_Installer{ $ocsid=OC_Appconfig::getValue( $app, 'ocsid', ''); if($ocsid<>'') { - - $ocsdata=OCSClient::getApplication($ocsid); + $ocsClient = new OCSClient( + \OC::$server->getHTTPClientService(), + \OC::$server->getConfig(), + \OC::$server->getLogger() + ); + $ocsdata = $ocsClient->getApplication($ocsid); $ocsversion= (string) $ocsdata['version']; $currentversion=OC_App::getAppVersion($app); if (version_compare($ocsversion, $currentversion, '>')) { diff --git a/lib/private/ocsclient.php b/lib/private/ocsclient.php index f69426ddafe..84469cb5e0d 100644 --- a/lib/private/ocsclient.php +++ b/lib/private/ocsclient.php @@ -32,36 +32,75 @@ namespace OC; +use OCP\Http\Client\IClientService; +use OCP\IConfig; +use OCP\ILogger; + /** - * This class provides an easy way for apps to store config values in the - * database. + * Class OCSClient is a class for communication with the ownCloud appstore + * + * @package OC */ - class OCSClient { + /** @var IClientService */ + private $httpClientService; + /** @var IConfig */ + private $config; + /** @var ILogger */ + private $logger; + + /** + * @param IClientService $httpClientService + * @param IConfig $config + * @param ILogger $logger + */ + public function __construct(IClientService $httpClientService, + IConfig $config, + ILogger $logger) { + $this->httpClientService = $httpClientService; + $this->config = $config; + $this->logger = $logger; + } /** * Returns whether the AppStore is enabled (i.e. because the AppStore is disabled for EE) * * @return bool */ - public static function isAppStoreEnabled() { - if (\OC::$server->getConfig()->getSystemValue('appstoreenabled', true) === false ) { - return false; - } - - return true; + public function isAppStoreEnabled() { + return $this->config->getSystemValue('appstoreenabled', true) === true; } /** * Get the url of the OCS AppStore server. * * @return string of the AppStore server - * - * This function returns the url of the OCS AppStore server. It´s possible - * to set it in the config file or it will fallback to the default */ - private static function getAppStoreURL() { - return \OC::$server->getConfig()->getSystemValue('appstoreurl', 'https://api.owncloud.com/v1'); + private function getAppStoreUrl() { + return $this->config->getSystemValue('appstoreurl', 'https://api.owncloud.com/v1'); + } + + /** + * @param string $body + * @param string $action + * @return null|\SimpleXMLElement + */ + private function loadData($body, $action) { + $loadEntities = libxml_disable_entity_loader(true); + $data = @simplexml_load_string($body); + libxml_disable_entity_loader($loadEntities); + + if($data === false) { + $this->logger->error( + sprintf('Could not get %s, content was no valid XML', $action), + [ + 'app' => 'core', + ] + ); + return null; + } + + return $data; } /** @@ -71,36 +110,41 @@ class OCSClient { * @note returns NULL if config value appstoreenabled is set to false * This function returns a list of all the application categories on the OCS server */ - public static function getCategories() { - if (!self::isAppStoreEnabled()) { + public function getCategories() { + if (!$this->isAppStoreEnabled()) { return null; } - $url = self::getAppStoreURL() . '/content/categories'; - $client = \OC::$server->getHTTPClientService()->newClient(); + $client = $this->httpClientService->newClient(); try { - $response = $client->get($url, ['timeout' => 5]); + $response = $client->get( + $this->getAppStoreUrl() . '/content/categories', + [ + 'timeout' => 5, + ] + ); } catch(\Exception $e) { + $this->logger->error( + sprintf('Could not get categories: %s', $e->getMessage()), + [ + 'app' => 'core', + ] + ); return null; } - if($response->getStatusCode() !== 200) { + $data = $this->loadData($response->getBody(), 'categories'); + if($data === null) { return null; } - $loadEntities = libxml_disable_entity_loader(true); - $data = simplexml_load_string($response->getBody()); - libxml_disable_entity_loader($loadEntities); - $tmp = $data->data; $cats = []; foreach ($tmp->category as $value) { - $id = (int)$value->id; $name = (string)$value->name; $cats[$id] = $name; - } return $cats; @@ -108,50 +152,54 @@ class OCSClient { /** * Get all the applications from the OCS server - * - * @return array|null an array of application data or null - * - * This function returns a list of all the applications on the OCS server - * @param array|string $categories + * @param array $categories * @param int $page * @param string $filter + * @return array An array of application data */ - public static function getApplications($categories, $page, $filter) { - if (!self::isAppStoreEnabled()) { - return (array()); + public function getApplications(array $categories, $page, $filter) { + if (!$this->isAppStoreEnabled()) { + return []; } - if (is_array($categories)) { - $categoriesString = implode('x', $categories); - } else { - $categoriesString = $categories; - } - - $version = '&version=' . implode('x', \OC_Util::getVersion()); - $filterUrl = '&filter=' . urlencode($filter); - $url = self::getAppStoreURL() . '/content/data?categories=' . urlencode($categoriesString) - . '&sortmode=new&page=' . urlencode($page) . '&pagesize=100' . $filterUrl . $version; - $apps = []; - - $client = \OC::$server->getHTTPClientService()->newClient(); + $client = $this->httpClientService->newClient(); try { - $response = $client->get($url, ['timeout' => 5]); + $response = $client->get( + $this->getAppStoreUrl() . '/content/data', + [ + 'timeout' => 5, + 'query' => [ + 'version' => implode('x', \OC_Util::getVersion()), + 'filter' => $filter, + 'categories' => implode('x', $categories), + 'sortmode' => 'new', + 'page' => $page, + 'pagesize' => 100, + 'approved' => $filter + ], + ] + ); } catch(\Exception $e) { - return null; + $this->logger->error( + sprintf('Could not get applications: %s', $e->getMessage()), + [ + 'app' => 'core', + ] + ); + return []; } - if($response->getStatusCode() !== 200) { - return null; + $data = $this->loadData($response->getBody(), 'applications'); + if($data === null) { + return []; } - $loadEntities = libxml_disable_entity_loader(true); - $data = simplexml_load_string($response->getBody()); - libxml_disable_entity_loader($loadEntities); - $tmp = $data->data->content; $tmpCount = count($tmp); + + $apps = []; for ($i = 0; $i < $tmpCount; $i++) { - $app = array(); + $app = []; $app['id'] = (string)$tmp[$i]->id; $app['name'] = (string)$tmp[$i]->name; $app['label'] = (string)$tmp[$i]->label; @@ -159,6 +207,7 @@ class OCSClient { $app['type'] = (string)$tmp[$i]->typeid; $app['typename'] = (string)$tmp[$i]->typename; $app['personid'] = (string)$tmp[$i]->personid; + $app['profilepage'] = (string)$tmp[$i]->profilepage; $app['license'] = (string)$tmp[$i]->license; $app['detailpage'] = (string)$tmp[$i]->detailpage; $app['preview'] = (string)$tmp[$i]->smallpreviewpic1; @@ -167,9 +216,11 @@ class OCSClient { $app['description'] = (string)$tmp[$i]->description; $app['score'] = (string)$tmp[$i]->score; $app['downloads'] = (int)$tmp[$i]->downloads; + $app['level'] = (int)$tmp[$i]->approved; $apps[] = $app; } + return $apps; } @@ -182,84 +233,94 @@ class OCSClient { * * This function returns an applications from the OCS server */ - public static function getApplication($id) { - if (!self::isAppStoreEnabled()) { + public function getApplication($id) { + if (!$this->isAppStoreEnabled()) { return null; } - $url = self::getAppStoreURL() . '/content/data/' . urlencode($id); - $client = \OC::$server->getHTTPClientService()->newClient(); + + $client = $this->httpClientService->newClient(); try { - $response = $client->get($url, ['timeout' => 5]); + $response = $client->get( + $this->getAppStoreUrl() . '/content/data/' . urlencode($id), + [ + 'timeout' => 5, + ] + ); } catch(\Exception $e) { + $this->logger->error( + sprintf('Could not get application: %s', $e->getMessage()), + [ + 'app' => 'core', + ] + ); return null; } - if($response->getStatusCode() !== 200) { + $data = $this->loadData($response->getBody(), 'application'); + if($data === null) { return null; } - $loadEntities = libxml_disable_entity_loader(true); - $data = simplexml_load_string($response->getBody()); - libxml_disable_entity_loader($loadEntities); - $tmp = $data->data->content; - if (is_null($tmp)) { - \OC_Log::write('core', 'Invalid OCS content returned for app ' . $id, \OC_Log::FATAL); - return null; - } + $app = []; - $app['id'] = $tmp->id; - $app['name'] = $tmp->name; - $app['version'] = $tmp->version; - $app['type'] = $tmp->typeid; - $app['label'] = $tmp->label; - $app['typename'] = $tmp->typename; - $app['personid'] = $tmp->personid; - $app['detailpage'] = $tmp->detailpage; - $app['preview1'] = $tmp->smallpreviewpic1; - $app['preview2'] = $tmp->smallpreviewpic2; - $app['preview3'] = $tmp->smallpreviewpic3; + $app['id'] = (int)$tmp->id; + $app['name'] = (string)$tmp->name; + $app['version'] = (string)$tmp->version; + $app['type'] = (string)$tmp->typeid; + $app['label'] = (string)$tmp->label; + $app['typename'] = (string)$tmp->typename; + $app['personid'] = (string)$tmp->personid; + $app['profilepage'] = (string)$tmp->profilepage; + $app['detailpage'] = (string)$tmp->detailpage; + $app['preview1'] = (string)$tmp->smallpreviewpic1; + $app['preview2'] = (string)$tmp->smallpreviewpic2; + $app['preview3'] = (string)$tmp->smallpreviewpic3; $app['changed'] = strtotime($tmp->changed); - $app['description'] = $tmp->description; - $app['detailpage'] = $tmp->detailpage; - $app['score'] = $tmp->score; + $app['description'] = (string)$tmp->description; + $app['detailpage'] = (string)$tmp->detailpage; + $app['score'] = (int)$tmp->score; return $app; } /** * Get the download url for an application from the OCS server - * + * @param $id * @return array|null an array of application data or null - * - * This function returns an download url for an applications from the OCS server - * @param string $id - * @param integer $item */ - public static function getApplicationDownload($id, $item) { - if (!self::isAppStoreEnabled()) { + public function getApplicationDownload($id) { + if (!$this->isAppStoreEnabled()) { return null; } - $url = self::getAppStoreURL() . '/content/download/' . urlencode($id) . '/' . urlencode($item); - $client = \OC::$server->getHTTPClientService()->newClient(); + $url = $this->getAppStoreUrl() . '/content/download/' . urlencode($id) . '/1'; + $client = $this->httpClientService->newClient(); try { - $response = $client->get($url, ['timeout' => 5]); + $response = $client->get( + $url, + [ + 'timeout' => 5, + ] + ); } catch(\Exception $e) { + $this->logger->error( + sprintf('Could not get application download URL: %s', $e->getMessage()), + [ + 'app' => 'core', + ] + ); return null; } - if($response->getStatusCode() !== 200) { + $data = $this->loadData($response->getBody(), 'application download URL'); + if($data === null) { return null; } - $loadEntities = libxml_disable_entity_loader(true); - $data = simplexml_load_string($response->getBody()); - libxml_disable_entity_loader($loadEntities); - $tmp = $data->data->content; - $app = array(); + $app = []; if (isset($tmp->downloadlink)) { - $app['downloadlink'] = $tmp->downloadlink; + $app['downloadlink'] = (string)$tmp->downloadlink; } else { $app['downloadlink'] = ''; } diff --git a/lib/private/preview.php b/lib/private/preview.php index f8e90cafbc1..eab60e10862 100644 --- a/lib/private/preview.php +++ b/lib/private/preview.php @@ -3,11 +3,11 @@ * @author Björn Schießle <schiessle@owncloud.com> * @author Frank Karlitschek <frank@owncloud.org> * @author Georg Ehrke <georg@owncloud.com> - * @author Georg Ehrke <georg@ownCloud.com> * @author Joas Schilling <nickvergessen@owncloud.com> * @author Jörn Friedrich Dreyer <jfd@butonic.de> * @author Lukas Reschke <lukas@owncloud.com> * @author Morris Jobke <hey@morrisjobke.de> + * @author Olivier Paroz <owncloud@interfasys.ch> * @author Robin Appelman <icewind@owncloud.com> * @author Robin McCorkell <rmccorkell@karoshi.org.uk> * @author Thomas Müller <thomas.mueller@tmit.eu> @@ -95,9 +95,9 @@ class Preview { $this->userView = new \OC\Files\View('/' . $user); //set config - $this->configMaxX = \OC_Config::getValue('preview_max_x', null); - $this->configMaxY = \OC_Config::getValue('preview_max_y', null); - $this->maxScaleFactor = \OC_Config::getValue('preview_max_scale_factor', 2); + $this->configMaxX = \OC::$server->getConfig()->getSystemValue('preview_max_x', 2048); + $this->configMaxY = \OC::$server->getConfig()->getSystemValue('preview_max_y', 2048); + $this->maxScaleFactor = \OC::$server->getConfig()->getSystemValue('preview_max_scale_factor', 2); //save parameters $this->setFile($file); @@ -246,7 +246,7 @@ class Preview { $configMaxX = $this->getConfigMaxX(); if (!is_null($configMaxX)) { if ($maxX > $configMaxX) { - \OC_Log::write('core', 'maxX reduced from ' . $maxX . ' to ' . $configMaxX, \OC_Log::DEBUG); + \OCP\Util::writeLog('core', 'maxX reduced from ' . $maxX . ' to ' . $configMaxX, \OCP\Util::DEBUG); $maxX = $configMaxX; } } @@ -267,7 +267,7 @@ class Preview { $configMaxY = $this->getConfigMaxY(); if (!is_null($configMaxY)) { if ($maxY > $configMaxY) { - \OC_Log::write('core', 'maxX reduced from ' . $maxY . ' to ' . $configMaxY, \OC_Log::DEBUG); + \OCP\Util::writeLog('core', 'maxX reduced from ' . $maxY . ' to ' . $configMaxY, \OCP\Util::DEBUG); $maxY = $configMaxY; } } @@ -304,12 +304,12 @@ class Preview { public function isFileValid() { $file = $this->getFile(); if ($file === '') { - \OC_Log::write('core', 'No filename passed', \OC_Log::DEBUG); + \OCP\Util::writeLog('core', 'No filename passed', \OCP\Util::DEBUG); return false; } if (!$this->fileView->file_exists($file)) { - \OC_Log::write('core', 'File:"' . $file . '" not found', \OC_Log::DEBUG); + \OCP\Util::writeLog('core', 'File:"' . $file . '" not found', \OCP\Util::DEBUG); return false; } @@ -321,9 +321,7 @@ class Preview { * @return bool */ public function deletePreview() { - $file = $this->getFile(); - - $fileInfo = $this->getFileInfo($file); + $fileInfo = $this->getFileInfo(); if($fileInfo !== null && $fileInfo !== false) { $fileId = $fileInfo->getId(); @@ -357,7 +355,8 @@ class Preview { } /** - * check if thumbnail or bigger version of thumbnail of file is cached + * Checks if thumbnail or bigger version of thumbnail of file is already cached + * * @param int $fileId fileId of the original image * @return string|false path to thumbnail if it exists or false */ @@ -366,9 +365,11 @@ class Preview { return false; } + // This gives us a calculated path to a preview of asked dimensions + // thumbnailFolder/fileId/my_image-<maxX>-<maxY>.png $preview = $this->buildCachePath($fileId); - //does a preview with the wanted height and width already exist? + // This checks if a preview exists at that location if ($this->userView->file_exists($preview)) { return $preview; } @@ -377,34 +378,39 @@ class Preview { } /** - * check if a bigger version of thumbnail of file is cached + * 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 + * * @return string|false path to bigger thumbnail if it exists or false - */ + */ private function isCachedBigger($fileId) { if (is_null($fileId)) { return false; } - // in order to not loose quality we better generate aspect preserving previews from the original file - if ($this->keepAspect) { - return false; - } - $maxX = $this->getMaxX(); //array for usable cached thumbnails + // FIXME: Checking only the width could lead to issues $possibleThumbnails = $this->getPossibleThumbnails($fileId); foreach ($possibleThumbnails as $width => $path) { - if ($width < $maxX) { + if ($width === 'max' || $width < $maxX) { continue; } else { return $path; } } + // At this stage, we didn't find a preview, so if the folder is not empty, + // we return the max preview we generated on the first run + if ($possibleThumbnails) { + return $possibleThumbnails['max']; + } + return false; } @@ -421,7 +427,7 @@ class Preview { $previewPath = $this->getPreviewPath($fileId); - $wantedAspectRatio = (float) ($this->getMaxX() / $this->getMaxY()); + $wantedAspectRatio = (float)($this->getMaxX() / $this->getMaxY()); //array for usable cached thumbnails $possibleThumbnails = array(); @@ -429,6 +435,11 @@ class Preview { $allThumbnails = $this->userView->getDirectoryContent($previewPath); foreach ($allThumbnails as $thumbnail) { $name = rtrim($thumbnail['name'], '.png'); + // Always add the max preview to the array + if (strpos($name, 'max')) { + $possibleThumbnails['max'] = $thumbnail['path']; + continue; + } list($x, $y, $aspectRatio) = $this->getDimensionsFromFilename($name); if (abs($aspectRatio - $wantedAspectRatio) >= 0.000001 @@ -482,7 +493,11 @@ class Preview { } /** - * return a preview of a file + * 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() { @@ -491,76 +506,22 @@ class Preview { } $this->preview = null; - $file = $this->getFile(); - $maxX = $this->getMaxX(); - $maxY = $this->getMaxY(); - $scalingUp = $this->getScalingUp(); - - $fileInfo = $this->getFileInfo($file); - if($fileInfo === null || $fileInfo === false) { + $fileInfo = $this->getFileInfo(); + if ($fileInfo === null || $fileInfo === false) { return new \OC_Image(); } - $fileId = $fileInfo->getId(); + $fileId = $fileInfo->getId(); $cached = $this->isCached($fileId); if ($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; - - $this->resizeAndCrop(); - fclose($stream); - } + $this->getCachedPreview($fileId, $cached); } if (is_null($this->preview)) { - $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; - } - - \OC_Log::write('core', 'Generating preview for "' . $file . '" with "' . get_class($provider) . '"', \OC_Log::DEBUG); - - /** @var $provider Provider */ - $preview = $provider->getThumbnail($file, $maxX, $maxY, $scalingUp, $this->fileView); - - if (!($preview instanceof \OCP\IImage)) { - continue; - } - - $this->preview = $preview; - $this->resizeAndCrop(); - - $previewPath = $this->getPreviewPath($fileId); - $cachePath = $this->buildCachePath($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->userView->file_put_contents($cachePath, $preview->data()); - - break 2; - } - } + $this->generatePreview($fileId); } + // We still don't have a preview, so we generate an empty object which can't be displayed if (is_null($this->preview)) { $this->preview = new \OC_Image(); } @@ -588,18 +549,58 @@ class Preview { } /** + * 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; + + $maxX = (int)$this->getMaxX(); + $maxY = (int)$this->getMaxY(); + $previewX = (int)$this->preview->width(); + $previewY = (int)$this->preview->height(); + + 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) { + // Resize and store + $this->resizeAndCrop(); + // We save a copy in the cache to speed up future calls + $cachePath = $this->buildCachePath($fileId); + $this->userView->file_put_contents($cachePath, $this->preview->data()); + } + + /** * resize, crop and fix orientation - * @return void + * + * @param bool $max */ - private function resizeAndCrop() { + private function resizeAndCrop($max = false) { $image = $this->preview; - $x = $this->getMaxX(); - $y = $this->getMaxY(); - $scalingUp = $this->getScalingUp(); - $maxScaleFactor = $this->getMaxScaleFactor(); + + list($x, $y, $scalingUp, $maxScaleFactor) = $this->getResizeData($max); if (!($image instanceof \OCP\IImage)) { - \OC_Log::write('core', '$this->preview is not an instance of \OCP\IImage', \OC_Log::DEBUG); + \OCP\Util::writeLog('core', '$this->preview is not an instance of OC_Image', \OCP\Util::DEBUG); return; } @@ -617,6 +618,7 @@ class Preview { } } + // The preview already has the asked dimensions if ($x === $realX && $y === $realY) { $this->preview = $image; return; @@ -639,7 +641,7 @@ class Preview { if (!is_null($maxScaleFactor)) { if ($factor > $maxScaleFactor) { - \OC_Log::write('core', 'scale factor reduced from ' . $factor . ' to ' . $maxScaleFactor, \OC_Log::DEBUG); + \OCP\Util::writeLog('core', 'scale factor reduced from ' . $factor . ' to ' . $maxScaleFactor, \OCP\Util::DEBUG); $factor = $maxScaleFactor; } } @@ -649,11 +651,13 @@ class Preview { $image->preciseResize($newXSize, $newYSize); + // The preview has been upscaled and now has the asked dimensions if ($newXSize === $x && $newYSize === $y) { $this->preview = $image; return; } + // One dimension of the upscaled preview is too big if ($newXSize >= $x && $newYSize >= $y) { $cropX = floor(abs($x - $newXSize) * 0.5); //don't crop previews on the Y axis, this sucks if it's a document. @@ -666,6 +670,7 @@ class Preview { return; } + // One dimension of the upscaled preview is too small and we're allowed to scale up if (($newXSize < $x || $newYSize < $y) && $scalingUp) { if ($newXSize > $x) { $cropX = floor(($newXSize - $x) * 0.5); @@ -698,11 +703,161 @@ class Preview { $image = new \OC_Image($backgroundLayer); $this->preview = $image; + return; } } /** + * Returns data to be used to resize a preview + * + * @param $max + * + * @return array + */ + private function getResizeData($max) { + if (!$max) { + $x = $this->getMaxX(); + $y = $this->getMaxY(); + $scalingUp = $this->getScalingUp(); + $maxScaleFactor = $this->getMaxScaleFactor(); + } else { + $x = $this->configMaxX; + $y = $this->configMaxY; + $scalingUp = false; + $maxScaleFactor =1; + } + + return [$x, $y, $scalingUp, $maxScaleFactor]; + } + + /** + * Returns the path to a preview based on its dimensions and aspect + * + * @param int $fileId + * + * @return string + */ + private function buildCachePath($fileId) { + $maxX = $this->getMaxX(); + $maxY = $this->getMaxY(); + + $previewPath = $this->getPreviewPath($fileId); + $previewPath = $previewPath . strval($maxX) . '-' . strval($maxY); + if ($this->keepAspect) { + $previewPath .= '-with-aspect'; + } + $previewPath .= '.png'; + + return $previewPath; + } + + /** + * @param int $fileId + * + * @return string + */ + private function getPreviewPath($fileId) { + return $this->getThumbnailsFolder() . '/' . $fileId . '/'; + } + + /** + * Asks the provider to send a preview of the file of maximum dimensions + * 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->configMaxX, $this->configMaxY, $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); + } + } + + /** + * Stores the max preview in the cache + * + * @param string $previewPath path to the preview + */ + private function storeMaxPreview($previewPath) { + $maxPreview = 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')) { + $maxPreview = true; + break; + } + } + // We haven't found the max preview, so we create it + if (!$maxPreview) { + // Most providers don't resize their thumbnails yet + $this->resizeAndCrop(true); + + $maxX = $preview->width(); + $maxY = $preview->height(); + $previewPath = $previewPath . strval($maxX) . '-' . strval($maxY); + $previewPath .= '-max.png'; + $this->userView->file_put_contents($previewPath, $preview->data()); + } + } + + /** * @param array $args */ public static function post_write($args) { @@ -791,30 +946,4 @@ class Preview { $preview->deleteAllPreviews(); } - /** - * @param int $fileId - * @return string - */ - private function buildCachePath($fileId) { - $maxX = $this->getMaxX(); - $maxY = $this->getMaxY(); - - $previewPath = $this->getPreviewPath($fileId); - $preview = $previewPath . strval($maxX) . '-' . strval($maxY); - if ($this->keepAspect) { - $preview .= '-with-aspect'; - } - $preview .= '.png'; - - return $preview; - } - - - /** - * @param int $fileId - * @return string - */ - private function getPreviewPath($fileId) { - return $this->getThumbnailsFolder() . '/' . $fileId . '/'; - } } diff --git a/lib/private/server.php b/lib/private/server.php index 661aaf6786d..6df7722973e 100644 --- a/lib/private/server.php +++ b/lib/private/server.php @@ -228,8 +228,12 @@ class Server extends SimpleContainer implements IServerContainer { new ArrayCache() ); }); - $this->registerService('ActivityManager', function ($c) { - return new ActivityManager(); + $this->registerService('ActivityManager', function (Server $c) { + return new ActivityManager( + $c->getRequest(), + $c->getUserSession(), + $c->getConfig() + ); }); $this->registerService('AvatarManager', function ($c) { return new AvatarManager(); @@ -396,6 +400,13 @@ class Server extends SimpleContainer implements IServerContainer { new \OC_Defaults() ); }); + $this->registerService('OcsClient', function(Server $c) { + return new OCSClient( + $this->getHTTPClientService(), + $this->getConfig(), + $this->getLogger() + ); + }); } /** @@ -435,7 +446,7 @@ class Server extends SimpleContainer implements IServerContainer { * currently being processed is returned from this method. * In case the current execution was not initiated by a web request null is returned * - * @return \OCP\IRequest|null + * @return \OCP\IRequest */ function getRequest() { return $this->query('Request'); @@ -837,6 +848,13 @@ class Server extends SimpleContainer implements IServerContainer { } /** + * @return \OC\OCSClient + */ + public function getOcsClient() { + return $this->query('OcsClient'); + } + + /** * @return \OCP\IDateTimeZone */ public function getDateTimeZone() { diff --git a/lib/private/share/helper.php b/lib/private/share/helper.php index 5345c8a018f..65167dd7549 100644 --- a/lib/private/share/helper.php +++ b/lib/private/share/helper.php @@ -50,34 +50,19 @@ class Helper extends \OC\Share\Constants { } return $backend->generateTarget($itemSource, false); } else { - if ($itemType == 'file' || $itemType == 'folder') { - $column = 'file_target'; - $columnSource = 'file_source'; - } else { - $column = 'item_target'; - $columnSource = 'item_source'; - } if ($shareType == self::SHARE_TYPE_USER) { // Share with is a user, so set share type to user and groups $shareType = self::$shareTypeUserAndGroups; } - $exclude = array(); - - $result = \OCP\Share::getItemsSharedWithUser($itemType, $shareWith); - foreach ($result as $row) { - if ($row['permissions'] > 0) { - $exclude[] = $row[$column]; - } - } // Check if suggested target exists first if (!isset($suggestedTarget)) { $suggestedTarget = $itemSource; } if ($shareType == self::SHARE_TYPE_GROUP) { - $target = $backend->generateTarget($suggestedTarget, false, $exclude); + $target = $backend->generateTarget($suggestedTarget, false); } else { - $target = $backend->generateTarget($suggestedTarget, $shareWith, $exclude); + $target = $backend->generateTarget($suggestedTarget, $shareWith); } return $target; diff --git a/lib/private/share/share.php b/lib/private/share/share.php index 98c612d5eb6..729dbe79d38 100644 --- a/lib/private/share/share.php +++ b/lib/private/share/share.php @@ -37,6 +37,10 @@ namespace OC\Share; +use OCP\IUserSession; +use OC\DB\Connection; +use OCP\IConfig; + /** * This class provides the ability for apps to share their content between users. * Apps must create a backend class that implements OCP\Share_Backend and register it with this class. @@ -1151,6 +1155,78 @@ class Share extends \OC\Share\Constants { } /** + * Retrieve the owner of a connection + * + * @param Connection $connection + * @param int $shareId + * @throws \Exception + * @return string uid of share owner + */ + private static function getShareOwner(Connection $connection, $shareId) { + $qb = $connection->createQueryBuilder(); + + $qb->select('`uid_owner`') + ->from('`*PREFIX*share`') + ->where('`id` = :shareId') + ->setParameter(':shareId', $shareId); + $result = $qb->execute(); + $result = $result->fetch(); + + if (empty($result)) { + throw new \Exception('Share not found'); + } + + return $result['uid_owner']; + } + + /** + * Set expiration date for a share + * + * @param IUserSession $userSession + * @param Connection $connection + * @param IConfig $config + * @param int $shareId + * @param string $password + * @throws \Exception + * @return boolean + */ + public static function setPassword(IUserSession $userSession, + Connection $connection, + IConfig $config, + $shareId, $password) { + $user = $userSession->getUser(); + if (is_null($user)) { + throw new \Exception("User not logged in"); + } + + $uid = self::getShareOwner($connection, $shareId); + + if ($uid !== $user->getUID()) { + throw new \Exception('Cannot update share of a different user'); + } + + if ($password === '') { + $password = null; + } + + //If passwords are enforced the password can't be null + if (self::enforcePassword($config) && is_null($password)) { + throw new \Exception('Cannot remove password'); + } + + $qb = $connection->createQueryBuilder(); + $qb->update('`*PREFIX*share`') + ->set('`share_with`', ':pass') + ->where('`id` = :shareId') + ->setParameter(':pass', is_null($password) ? 'NULL' : $qb->expr()->literal(\OC::$server->getHasher()->hash($password))) + ->setParameter(':shareId', $shareId); + + $qb->execute(); + + return true; + } + + /** * Checks whether a share has expired, calls unshareItem() if yes. * @param array $item Share data (usually database row) * @return boolean True if item was expired, false otherwise. @@ -1829,7 +1905,11 @@ class Share extends \OC\Share\Constants { $isGroupShare = false; if ($shareType == self::SHARE_TYPE_GROUP) { $isGroupShare = true; - $users = \OC_Group::usersInGroup($shareWith['group']); + if (isset($shareWith['users'])) { + $users = $shareWith['users']; + } else { + $users = \OC_Group::usersInGroup($shareWith['group']); + } // remove current user from list if (in_array(\OCP\User::getUser(), $users)) { unset($users[array_search(\OCP\User::getUser(), $users)]); @@ -1940,7 +2020,8 @@ class Share extends \OC\Share\Constants { $fileTarget = null; } - if ($itemTarget === $groupItemTarget && (isset($fileSource) && $fileTarget === $groupItemTarget)) { + if (($itemTarget === $groupItemTarget) && + (!isset($fileSource) || $fileTarget === $groupFileTarget)) { continue; } } @@ -2429,4 +2510,12 @@ class Share extends \OC\Share\Constants { return false; } + /** + * @param IConfig $config + * @return bool + */ + public static function enforcePassword(IConfig $config) { + $enforcePassword = $config->getAppValue('core', 'shareapi_enforce_links_password', 'no'); + return ($enforcePassword === "yes") ? true : false; + } } diff --git a/lib/private/tags.php b/lib/private/tags.php index 6a66cbde4b3..975b6dbfe0d 100644 --- a/lib/private/tags.php +++ b/lib/private/tags.php @@ -133,8 +133,6 @@ class Tags implements \OCP\ITags { if(count($defaultTags) > 0 && count($this->tags) === 0) { $this->addMultiple($defaultTags, true); } - \OCP\Util::writeLog('core', __METHOD__.', tags: ' . print_r($this->tags, true), - \OCP\Util::DEBUG); } /** diff --git a/lib/private/templatelayout.php b/lib/private/templatelayout.php index ee1412fba74..448276ca7fe 100644 --- a/lib/private/templatelayout.php +++ b/lib/private/templatelayout.php @@ -107,7 +107,7 @@ class OC_TemplateLayout extends OC_Template { $userDisplayName = OC_User::getDisplayName(); $this->assign('user_displayname', $userDisplayName); $this->assign('user_uid', OC_User::getUser()); - $this->assign('appsmanagement_active', strpos(\OC::$server->getRequest()->getRequestUri(), OC_Helper::linkToRoute('settings_apps')) === 0 ); + $this->assign('appsmanagement_active', strpos(\OC::$server->getRequest()->getRequestUri(), \OC::$server->getURLGenerator()->linkToRoute('settings.AppSettings.viewApps')) === 0 ); $this->assign('enableAvatars', $this->config->getSystemValue('enable_avatars', true)); $this->assign('userAvatarSet', \OC_Helper::userAvatarSet(OC_User::getUser())); } else if ($renderAs == 'error') { diff --git a/lib/private/util.php b/lib/private/util.php index 3fd0f844684..102dc8c59db 100644 --- a/lib/private/util.php +++ b/lib/private/util.php @@ -566,6 +566,19 @@ class OC_Util { $webServerRestart = true; } + // Check if server running on Windows platform + if(OC_Util::runningOnWindows()) { + $errors[] = [ + 'error' => $l->t('Microsoft Windows Platform is not supported'), + 'hint' => $l->t('Running ownCloud Server on the Microsoft Windows platform is not supported. We suggest you ' . + 'use a Linux server in a virtual machine if you have no option for migrating the server itself. ' . + 'Find Linux packages as well as easy to deploy virtual machine images on <a href="%s">%s</a>. ' . + 'For migrating existing installations to Linux you can find some tips and a migration script ' . + 'in <a href="%s">our documentation</a>.', + ['https://owncloud.org/install/', 'owncloud.org/install/', 'https://owncloud.org/?p=8045']) + ]; + } + // Check if config folder is writable. if (!is_writable(OC::$configDir) or !is_readable(OC::$configDir)) { $errors[] = array( diff --git a/lib/public/activity/imanager.php b/lib/public/activity/imanager.php index f7885860c4a..2e55c8b45b2 100644 --- a/lib/public/activity/imanager.php +++ b/lib/public/activity/imanager.php @@ -136,4 +136,14 @@ interface IManager { * @return array */ function getQueryForFilter($filter); + + /** + * Get the user we need to use + * + * Either the user is logged in, or we try to get it from the token + * + * @return string + * @throws \UnexpectedValueException If the token is invalid, does not exist or is not unique + */ + public function getCurrentUserId(); } diff --git a/lib/public/app/iappmanager.php b/lib/public/app/iappmanager.php index f50a7f64174..69b8c335d67 100644 --- a/lib/public/app/iappmanager.php +++ b/lib/public/app/iappmanager.php @@ -78,4 +78,9 @@ interface IAppManager { * @return string[] */ public function getInstalledApps(); + + /** + * Clear the cached list of apps when enabling/disabling an app + */ + public function clearAppsCache(); } diff --git a/lib/public/encryption/exceptions/genericencryptionexception.php b/lib/public/encryption/exceptions/genericencryptionexception.php index b7addd3b0c1..c488d4df162 100644 --- a/lib/public/encryption/exceptions/genericencryptionexception.php +++ b/lib/public/encryption/exceptions/genericencryptionexception.php @@ -1,7 +1,8 @@ <?php - /** - * @author Clark Tomlinson <clark@owncloud.com> - * @since 2/25/15, 9:30 AM +/** + * @author Clark Tomlinson <fallen013@gmail.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * @@ -24,7 +25,12 @@ namespace OCP\Encryption\Exceptions; class GenericEncryptionException extends \Exception { - public function __construct($message = "", $code = 0, \Exception $previous = null) { + /** + * @param string $message + * @param int $code + * @param \Exception $previous + */ + public function __construct($message = '', $code = 0, \Exception $previous = null) { if (empty($message)) { $message = 'Unspecified encryption exception'; } diff --git a/lib/public/encryption/iencryptionmodule.php b/lib/public/encryption/iencryptionmodule.php index 7265fee1259..c1ce7d99d78 100644 --- a/lib/public/encryption/iencryptionmodule.php +++ b/lib/public/encryption/iencryptionmodule.php @@ -1,24 +1,22 @@ <?php - /** - * ownCloud - public interface of ownCloud for encryption modules - * - * @copyright (C) 2015 ownCloud, Inc. + * @author Björn Schießle <schiessle@owncloud.com> * - * @author Bjoern Schiessle <schiessle@owncloud.com> + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. + * 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 library is distributed in the hope that it will be useful, + * 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. + * 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/> * - * You should have received a copy of the GNU Affero General Public - * License along with this library. If not, see <http://www.gnu.org/licenses/>. */ namespace OCP\Encryption; @@ -51,7 +49,7 @@ interface IEncryptionModule { * written to the header, in case of a write operation * or if no additional data is needed return a empty array */ - public function begin($path, $user, $header, $accessList); + public function begin($path, $user, array $header, array $accessList); /** * last chunk received. This is the place where you can perform some final @@ -88,7 +86,7 @@ interface IEncryptionModule { * @param array $accessList who has access to the file contains the key 'users' and 'public' * @return boolean */ - public function update($path, $uid, $accessList); + public function update($path, $uid, array $accessList); /** * should the file be encrypted or not diff --git a/lib/public/encryption/ifile.php b/lib/public/encryption/ifile.php index 464f41509d2..cc1e8f426b2 100644 --- a/lib/public/encryption/ifile.php +++ b/lib/public/encryption/ifile.php @@ -1,24 +1,23 @@ <?php - /** - * ownCloud - * - * @copyright (C) 2015 ownCloud, Inc. + * @author Björn Schießle <schiessle@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> * - * @author Bjoern Schiessle <schiessle@owncloud.com> + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. + * 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 library is distributed in the hope that it will be useful, + * 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. + * 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/> * - * You should have received a copy of the GNU Affero General Public - * License along with this library. If not, see <http://www.gnu.org/licenses/>. */ namespace OCP\Encryption; diff --git a/lib/public/encryption/imanager.php b/lib/public/encryption/imanager.php index 2691604ac37..3dcdbf5d03a 100644 --- a/lib/public/encryption/imanager.php +++ b/lib/public/encryption/imanager.php @@ -1,24 +1,22 @@ <?php - /** - * ownCloud - manage encryption modules - * - * @copyright (C) 2015 ownCloud, Inc. + * @author Björn Schießle <schiessle@owncloud.com> * - * @author Bjoern Schiessle <schiessle@owncloud.com> + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. + * 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 library is distributed in the hope that it will be useful, + * 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. + * 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/> * - * You should have received a copy of the GNU Affero General Public - * License along with this library. If not, see <http://www.gnu.org/licenses/>. */ namespace OCP\Encryption; @@ -75,7 +73,7 @@ interface IManager { * get default encryption module * * @return \OCP\Encryption\IEncryptionModule - * @throws Exceptions\ModuleDoesNotExistsException + * @throws ModuleDoesNotExistsException */ public function getDefaultEncryptionModule(); diff --git a/lib/public/encryption/keys/istorage.php b/lib/public/encryption/keys/istorage.php index 2d1672face5..c6933e7afab 100644 --- a/lib/public/encryption/keys/istorage.php +++ b/lib/public/encryption/keys/istorage.php @@ -1,24 +1,23 @@ <?php - /** - * ownCloud - * - * @copyright (C) 2015 ownCloud, Inc. + * @author Björn Schießle <schiessle@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> * - * @author Bjoern Schiessle <schiessle@owncloud.com> + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. + * 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 library is distributed in the hope that it will be useful, + * 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. + * 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/> * - * You should have received a copy of the GNU Affero General Public - * License along with this library. If not, see <http://www.gnu.org/licenses/>. */ namespace OCP\Encryption\Keys; @@ -90,7 +89,7 @@ interface IStorage { * @param string $uid ID if the user for whom we want to delete the key * @param string $keyId id of the key * - * @return boolean + * @return boolean False when the key could not be deleted */ public function deleteUserKey($uid, $keyId); @@ -100,7 +99,7 @@ interface IStorage { * @param string $path path to file * @param string $keyId id of the key * - * @return boolean + * @return boolean False when the key could not be deleted */ public function deleteFileKey($path, $keyId); @@ -108,7 +107,7 @@ interface IStorage { * delete all file keys for a given file * * @param string $path to the file - * @return boolean + * @return boolean False when the keys could not be deleted */ public function deleteAllFileKeys($path); @@ -118,7 +117,7 @@ interface IStorage { * * @param string $keyId id of the key * - * @return boolean + * @return boolean False when the key could not be deleted */ public function deleteSystemUserKey($keyId); diff --git a/lib/public/iconfig.php b/lib/public/iconfig.php index c63ba1a90a6..f28a114a2bb 100644 --- a/lib/public/iconfig.php +++ b/lib/public/iconfig.php @@ -133,7 +133,7 @@ interface IConfig { * @param string $userId the userId of the user that we want to store the value under * @param string $appName the appName that we stored the value under * @param string $key the key under which the value is being stored - * @param string $default the default value to be returned if the value isn't set + * @param mixed $default the default value to be returned if the value isn't set * @return string */ public function getUserValue($userId, $appName, $key, $default = ''); diff --git a/lib/public/iservercontainer.php b/lib/public/iservercontainer.php index 509e5894d47..dd0d2f417cf 100644 --- a/lib/public/iservercontainer.php +++ b/lib/public/iservercontainer.php @@ -2,6 +2,7 @@ /** * @author Bart Visscher <bartv@thisnet.nl> * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Björn Schießle <schiessle@owncloud.com> * @author Christopher Schäpers <kondou@ts.unde.re> * @author Jörn Friedrich Dreyer <jfd@butonic.de> * @author Lukas Reschke <lukas@owncloud.com> @@ -59,7 +60,7 @@ interface IServerContainer { * is returned from this method. * In case the current execution was not initiated by a web request null is returned * - * @return \OCP\IRequest|null + * @return \OCP\IRequest */ function getRequest(); diff --git a/lib/public/share.php b/lib/public/share.php index d27c1825d62..fcdcc6a6d2c 100644 --- a/lib/public/share.php +++ b/lib/public/share.php @@ -318,6 +318,20 @@ class Share extends \OC\Share\Constants { } /** + * Set expiration date for a share + * @param int $shareId + * @param string $password + * @return boolean + */ + public static function setPassword($shareId, $password) { + $userSession = \OC::$server->getUserSession(); + $connection = \OC::$server->getDatabaseConnection(); + $config = \OC::$server->getConfig(); + return \OC\Share\Share::setPassword($userSession, $connection, $config, $shareId, $password); + } + + + /** * Get the backend class for the specified item type * @param string $itemType * @return Share_Backend diff --git a/ocs/v1.php b/ocs/v1.php index 5bba65d9a1a..398a128c64b 100644 --- a/ocs/v1.php +++ b/ocs/v1.php @@ -27,7 +27,7 @@ require_once '../lib/base.php'; -if (\OCP\Util::needUpgrade()) { +if (\OCP\Util::needUpgrade() || \OC::$server->getSystemConfig()->getValue('maintenance', false)) { // since the behavior of apps or remotes are unpredictable during // an upgrade, return a 503 directly OC_Response::setStatus(OC_Response::STATUS_SERVICE_UNAVAILABLE); diff --git a/settings/application.php b/settings/application.php index eb8f0f7a999..be127da31ac 100644 --- a/settings/application.php +++ b/settings/application.php @@ -71,7 +71,10 @@ class Application extends App { $c->query('Request'), $c->query('L10N'), $c->query('Config'), - $c->query('ICacheFactory') + $c->query('ICacheFactory'), + $c->query('INavigationManager'), + $c->query('IAppManager'), + $c->query('OcsClient') ); }); $container->registerService('SecuritySettingsController', function(IContainer $c) { @@ -192,6 +195,15 @@ class Application extends App { $container->registerService('ClientService', function(IContainer $c) { return $c->query('ServerContainer')->getHTTPClientService(); }); + $container->registerService('INavigationManager', function(IContainer $c) { + return $c->query('ServerContainer')->getNavigationManager(); + }); + $container->registerService('IAppManager', function(IContainer $c) { + return $c->query('ServerContainer')->getAppManager(); + }); + $container->registerService('OcsClient', function(IContainer $c) { + return $c->query('ServerContainer')->getOcsClient(); + }); $container->registerService('Util', function(IContainer $c) { return new \OC_Util(); }); diff --git a/settings/apps.php b/settings/apps.php deleted file mode 100644 index 7245b6610e0..00000000000 --- a/settings/apps.php +++ /dev/null @@ -1,42 +0,0 @@ -<?php -/** - * @author Bart Visscher <bartv@thisnet.nl> - * @author Frank Karlitschek <frank@owncloud.org> - * @author Jan-Christoph Borchardt <hey@jancborchardt.net> - * @author Lukas Reschke <lukas@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2015, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -OC_Util::checkAdminUser(); -\OC::$server->getSession()->close(); - -// Load the files we need -\OC_Util::addVendorScript('handlebars/handlebars'); -\OCP\Util::addScript("settings", "settings"); -\OCP\Util::addStyle("settings", "settings"); -\OC_Util::addVendorScript('select2/select2'); -\OC_Util::addVendorStyle('select2/select2'); -\OCP\Util::addScript("settings", "apps"); -\OC_App::setActiveNavigationEntry( "core_apps" ); - -$tmpl = new OC_Template( "settings", "apps", "user" ); -$tmpl->printPage(); - diff --git a/settings/controller/appsettingscontroller.php b/settings/controller/appsettingscontroller.php index 9a85f6d3b97..f1b62bb1d38 100644 --- a/settings/controller/appsettingscontroller.php +++ b/settings/controller/appsettingscontroller.php @@ -27,8 +27,13 @@ namespace OC\Settings\Controller; use OC\App\DependencyAnalyzer; use OC\App\Platform; use OC\OCSClient; +use OCP\App\IAppManager; use \OCP\AppFramework\Controller; +use OCP\AppFramework\Http\ContentSecurityPolicy; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\TemplateResponse; use OCP\ICacheFactory; +use OCP\INavigationManager; use OCP\IRequest; use OCP\IL10N; use OCP\IConfig; @@ -44,6 +49,12 @@ class AppSettingsController extends Controller { private $config; /** @var \OCP\ICache */ private $cache; + /** @var INavigationManager */ + private $navigationManager; + /** @var IAppManager */ + private $appManager; + /** @var OCSClient */ + private $ocsClient; /** * @param string $appName @@ -51,16 +62,53 @@ class AppSettingsController extends Controller { * @param IL10N $l10n * @param IConfig $config * @param ICacheFactory $cache + * @param INavigationManager $navigationManager + * @param IAppManager $appManager + * @param OCSClient $ocsClient */ public function __construct($appName, IRequest $request, IL10N $l10n, IConfig $config, - ICacheFactory $cache) { + ICacheFactory $cache, + INavigationManager $navigationManager, + IAppManager $appManager, + OCSClient $ocsClient) { parent::__construct($appName, $request); $this->l10n = $l10n; $this->config = $config; $this->cache = $cache->create($appName); + $this->navigationManager = $navigationManager; + $this->appManager = $appManager; + $this->ocsClient = $ocsClient; + } + + /** + * Enables or disables the display of experimental apps + * @param bool $state + * @return DataResponse + */ + public function changeExperimentalConfigState($state) { + $this->config->setSystemValue('appstore.experimental.enabled', $state); + $this->appManager->clearAppsCache(); + return new DataResponse(); + } + + /** + * @NoCSRFRequired + * @return TemplateResponse + */ + public function viewApps() { + $params = []; + $params['experimentalEnabled'] = $this->config->getSystemValue('appstore.experimental.enabled', false); + $this->navigationManager->setActiveEntry('core_apps'); + + $templateResponse = new TemplateResponse($this->appName, 'apps', $params, 'user'); + $policy = new ContentSecurityPolicy(); + $policy->addAllowedImageDomain('https://apps.owncloud.com'); + $templateResponse->setContentSecurityPolicy($policy); + + return $templateResponse; } /** @@ -77,16 +125,15 @@ class AppSettingsController extends Controller { ['id' => 1, 'displayName' => (string)$this->l10n->t('Not enabled')], ]; - if(OCSClient::isAppStoreEnabled()) { - $categories[] = ['id' => 2, 'displayName' => (string)$this->l10n->t('Recommended')]; + if($this->ocsClient->isAppStoreEnabled()) { // apps from external repo via OCS - $ocs = OCSClient::getCategories(); + $ocs = $this->ocsClient->getCategories(); if ($ocs) { foreach($ocs as $k => $v) { - $categories[] = array( + $categories[] = [ 'id' => $k, 'displayName' => str_replace('ownCloud ', '', $v) - ); + ]; } } } @@ -97,7 +144,8 @@ class AppSettingsController extends Controller { } /** - * Get all available categories + * Get all available apps in a category + * * @param int $category * @return array */ @@ -134,16 +182,9 @@ class AppSettingsController extends Controller { }); break; default: - if ($category === 2) { - $apps = \OC_App::getAppstoreApps('approved'); - if ($apps) { - $apps = array_filter($apps, function ($app) { - return isset($app['internalclass']) && $app['internalclass'] === 'recommendedapp'; - }); - } - } else { - $apps = \OC_App::getAppstoreApps('approved', $category); - } + $filter = $this->config->getSystemValue('appstore.experimental.enabled', false) ? 'all' : 'approved'; + + $apps = \OC_App::getAppstoreApps($filter, $category); if (!$apps) { $apps = array(); } else { diff --git a/settings/css/settings.css b/settings/css/settings.css index 3bc0a442f06..747e370617e 100644 --- a/settings/css/settings.css +++ b/settings/css/settings.css @@ -203,22 +203,52 @@ input.userFilter {width: 200px;} background: #fbb; } -.recommendedapp { - font-size: 11px; - background-position: left center; - padding-left: 18px; - vertical-align: top; +span.version { + margin-left: 1em; + margin-right: 1em; + color: #555; } -span.version { margin-left:1em; margin-right:1em; color:#555; } #app-navigation .app-external, -.app-version, -.recommendedapp { +.app-version { -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; filter: alpha(opacity=50); opacity: .5; } +.app-level { + margin-top: 8px; +} +.app-level span { + color: #555; + background-color: transparent; + border: 1px solid #555; + border-radius: 3px; + padding: 3px 6px; +} +.app-level .official { + border-color: #37ce02; + background-position: left center; + background-position: 5px center; + padding-left: 25px; +} +.app-level .approved { + border-color: #e8c805; +} +.app-level .experimental { + background-color: #ce3702; + border-color: #ce3702; + color: #fff; +} +.apps-experimental { + color: #ce3702; +} + +.app-score { + position: relative; + top: 4px; +} + #apps-list { position: relative; height: 100%; @@ -226,6 +256,9 @@ span.version { margin-left:1em; margin-right:1em; color:#555; } .section { position: relative; } +.section h2.app-name { + margin-bottom: 8px; +} .app-image { float: left; padding-right: 10px; @@ -245,7 +278,7 @@ span.version { margin-left:1em; margin-right:1em; color:#555; } .app-name, .app-version, .app-score, -.recommendedapp { +.app-level { display: inline-block; } @@ -270,13 +303,17 @@ span.version { margin-left:1em; margin-right:1em; color:#555; } white-space: pre-line; } -#app-category-2 { +#app-category-1 { border-bottom: 1px solid #e8e8e8; } +/* capitalize "Other" category */ +#app-category-925 { + text-transform: capitalize; +} .app-dependencies { margin-top: 10px; - color: #c33; + color: #ce3702; } .missing-dependencies { @@ -311,8 +348,9 @@ table.grid td.date{ #security-warning li { list-style: initial; margin: 10px 0; - color: #c33; + color: #ce3702; } + #shareAPI p { padding-bottom: 0.8em; } #shareAPI input#shareapiExpireAfterNDays {width: 25px;} #shareAPI .indent { @@ -321,6 +359,13 @@ table.grid td.date{ #shareAPI .double-indent { padding-left: 56px; } +#fileSharingSettings h3 { + display: inline-block; +} + +.icon-info { + padding: 11px 20px; +} .mail_settings p label:first-child { display: inline-block; @@ -362,12 +407,12 @@ table.grid td.date{ } span.success { - background: #37ce02; - border-radius: 3px; + background: #37ce02; + border-radius: 3px; } span.error { - background: #ce3702; + background: #ce3702; } diff --git a/settings/js/apps.js b/settings/js/apps.js index 3db84e8acd5..1bd7ffdf790 100644 --- a/settings/js/apps.js +++ b/settings/js/apps.js @@ -9,6 +9,17 @@ Handlebars.registerHelper('score', function() { } return new Handlebars.SafeString(''); }); +Handlebars.registerHelper('level', function() { + if(typeof this.level !== 'undefined') { + if(this.level === 200) { + return new Handlebars.SafeString('<span class="official icon-checkmark">Official</span>'); + } else if(this.level === 100) { + return new Handlebars.SafeString('<span class="approved">Approved</span>'); + } else { + return new Handlebars.SafeString('<span class="experimental">Experimental</span>'); + } + } +}); OC.Settings = OC.Settings || {}; OC.Settings.Apps = OC.Settings.Apps || { @@ -73,7 +84,6 @@ OC.Settings.Apps = OC.Settings.Apps || { this._loadCategoryCall = $.ajax(OC.generateUrl('settings/apps/list?category={categoryId}', { categoryId: categoryId }), { - data:{}, type:'GET', success: function (apps) { OC.Settings.Apps.State.apps = _.indexBy(apps.apps, 'id'); @@ -81,13 +91,27 @@ OC.Settings.Apps = OC.Settings.Apps || { var template = Handlebars.compile(source); if (apps.apps.length) { + apps.apps.sort(function(a,b) { + return b.level - a.level; + }); + + var firstExperimental = false; _.each(apps.apps, function(app) { - OC.Settings.Apps.renderApp(app, template, null); + if(app.level === 0 && firstExperimental === false) { + firstExperimental = true; + OC.Settings.Apps.renderApp(app, template, null, true); + } else { + OC.Settings.Apps.renderApp(app, template, null, false); + } }); } else { $('#apps-list').addClass('hidden'); $('#apps-list-empty').removeClass('hidden'); } + + $('.app-level .official').tipsy({fallback: t('core', 'Official apps are developed by and within the ownCloud community. They offer functionality central to ownCloud and are ready for production use.')}); + $('.app-level .approved').tipsy({fallback: t('core', 'Approved apps are developed by trusted developers and have passed a cursory security check. They are actively maintained in an open code repository and their maintainers deem them to be stable for casual to normal use.')}); + $('.app-level .experimental').tipsy({fallback: t('core', 'This app is not checked for security issues and is new or known to be unstable. Install on your own risk.')}); }, complete: function() { $('#apps-list').removeClass('icon-loading'); @@ -95,7 +119,7 @@ OC.Settings.Apps = OC.Settings.Apps || { }); }, - renderApp: function(app, template, selector) { + renderApp: function(app, template, selector, firstExperimental) { if (!template) { var source = $("#app-template").html(); template = Handlebars.compile(source); @@ -103,6 +127,7 @@ OC.Settings.Apps = OC.Settings.Apps || { if (typeof app === 'string') { app = OC.Settings.Apps.State.apps[app]; } + app.firstExperimental = firstExperimental; var html = template(app); if (selector) { @@ -438,6 +463,16 @@ OC.Settings.Apps = OC.Settings.Apps || { $select.change(); }); + $(document).on('click', '#enable-experimental-apps', function () { + var state = $(this).prop('checked') + $.ajax(OC.generateUrl('settings/apps/experimental'), { + data: {state: state}, + type: 'POST', + success:function () { + location.reload(); + } + }); + }); } }; diff --git a/settings/routes.php b/settings/routes.php index af9ac1d8eea..1bb14812145 100644 --- a/settings/routes.php +++ b/settings/routes.php @@ -33,25 +33,27 @@ namespace OC\Settings; $application = new Application(); -$application->registerRoutes($this, array( - 'resources' => array( - 'groups' => array('url' => '/settings/users/groups'), - 'users' => array('url' => '/settings/users/users') - ), - 'routes' => array( - array('name' => 'MailSettings#setMailSettings', 'url' => '/settings/admin/mailsettings', 'verb' => 'POST'), - array('name' => 'MailSettings#storeCredentials', 'url' => '/settings/admin/mailsettings/credentials', 'verb' => 'POST'), - array('name' => 'MailSettings#sendTestMail', 'url' => '/settings/admin/mailtest', 'verb' => 'POST'), - array('name' => 'AppSettings#listCategories', 'url' => '/settings/apps/categories', 'verb' => 'GET'), - array('name' => 'AppSettings#listApps', 'url' => '/settings/apps/list', 'verb' => 'GET'), - array('name' => 'SecuritySettings#trustedDomains', 'url' => '/settings/admin/security/trustedDomains', 'verb' => 'POST'), - array('name' => 'Users#setMailAddress', 'url' => '/settings/users/{id}/mailAddress', 'verb' => 'PUT'), - array('name' => 'LogSettings#setLogLevel', 'url' => '/settings/admin/log/level', 'verb' => 'POST'), - array('name' => 'LogSettings#getEntries', 'url' => '/settings/admin/log/entries', 'verb' => 'GET'), - array('name' => 'LogSettings#download', 'url' => '/settings/admin/log/download', 'verb' => 'GET'), +$application->registerRoutes($this, [ + 'resources' => [ + 'groups' => ['url' => '/settings/users/groups'], + 'users' => ['url' => '/settings/users/users'] + ], + 'routes' => [ + ['name' => 'MailSettings#setMailSettings', 'url' => '/settings/admin/mailsettings', 'verb' => 'POST'], + ['name' => 'MailSettings#storeCredentials', 'url' => '/settings/admin/mailsettings/credentials', 'verb' => 'POST'], + ['name' => 'MailSettings#sendTestMail', 'url' => '/settings/admin/mailtest', 'verb' => 'POST'], + ['name' => 'AppSettings#listCategories', 'url' => '/settings/apps/categories', 'verb' => 'GET'], + ['name' => 'AppSettings#viewApps', 'url' => '/settings/apps', 'verb' => 'GET'], + ['name' => 'AppSettings#listApps', 'url' => '/settings/apps/list', 'verb' => 'GET'], + ['name' => 'AppSettings#changeExperimentalConfigState', 'url' => '/settings/apps/experimental', 'verb' => 'POST'], + ['name' => 'SecuritySettings#trustedDomains', 'url' => '/settings/admin/security/trustedDomains', 'verb' => 'POST'], + ['name' => 'Users#setMailAddress', 'url' => '/settings/users/{id}/mailAddress', 'verb' => 'PUT'], + ['name' => 'LogSettings#setLogLevel', 'url' => '/settings/admin/log/level', 'verb' => 'POST'], + ['name' => 'LogSettings#getEntries', 'url' => '/settings/admin/log/entries', 'verb' => 'GET'], + ['name' => 'LogSettings#download', 'url' => '/settings/admin/log/download', 'verb' => 'GET'], ['name' => 'CheckSetup#check', 'url' => '/settings/ajax/checksetup', 'verb' => 'GET'], - ) -)); + ] +]); /** @var $this \OCP\Route\IRouter */ @@ -62,8 +64,6 @@ $this->create('settings_personal', '/settings/personal') ->actionInclude('settings/personal.php'); $this->create('settings_users', '/settings/users') ->actionInclude('settings/users.php'); -$this->create('settings_apps', '/settings/apps') - ->actionInclude('settings/apps.php'); $this->create('settings_admin', '/settings/admin') ->actionInclude('settings/admin.php'); // Settings ajax actions diff --git a/settings/templates/admin.php b/settings/templates/admin.php index 1b8ab0e3819..4bc497df764 100644 --- a/settings/templates/admin.php +++ b/settings/templates/admin.php @@ -269,6 +269,10 @@ if ($_['cronErrors']) { endif; ?> </p> <?php endif; ?> + <a target="_blank" class="icon-info svg" + title="<?php p($l->t('Open documentation'));?>" + href="<?php p(link_to_docs('admin-background-jobs')); ?>"></a> + <p> <input type="radio" name="mode" value="ajax" id="backgroundjobs_ajax" <?php if ($_['backgroundjobs_mode'] === "ajax") { diff --git a/settings/templates/apps.php b/settings/templates/apps.php index a2fe5d9b63a..31de7fa2318 100644 --- a/settings/templates/apps.php +++ b/settings/templates/apps.php @@ -1,3 +1,27 @@ +<?php +style('settings', 'settings'); +vendor_style( + 'core', + [ + 'select2/select2', + ] +); +vendor_script( + 'core', + [ + 'handlebars/handlebars', + 'select2/select2' + ] +); +script( + 'settings', + [ + 'settings', + 'apps', + ] +); +/** @var array $_ */ +?> <script id="categories-template" type="text/x-handlebars-template"> {{#each this}} <li id="app-category-{{id}}" data-category-id="{{id}}" tabindex="0"> @@ -16,6 +40,18 @@ </script> <script id="app-template" type="text/x-handlebars"> + {{#if firstExperimental}} + <div class="section apps-experimental"> + <h2><?php p($l->t('Experimental applications ahead')) ?></h2> + <p> + <?php p($l->t('Experimental apps are not checked for security ' . + 'issues, new or known to be unstable and under heavy ' . + 'development. Installing them can cause data loss or security ' . + 'breaches.')) ?> + </p> + </div> + {{/if}} + <div class="section" id="app-{{id}}"> {{#if preview}} <div class="app-image{{#if previewAsIcon}} app-image-icon{{/if}} hidden"> @@ -23,17 +59,19 @@ {{/if}} <h2 class="app-name"><a href="{{detailpage}}" target="_blank">{{name}}</a></h2> <div class="app-version"> {{version}}</div> + {{#if profilepage}}<a href="{{profilepage}}" target="_blank" rel="noreferrer">{{/if}} <div class="app-author"><?php p($l->t('by')); ?> {{author}} {{#if licence}} ({{licence}}-<?php p($l->t('licensed')); ?>) {{/if}} </div> + {{#if profilepage}}</a>{{/if}} + <div class="app-level"> + {{{level}}} + </div> {{#if score}} <div class="app-score">{{{score}}}</div> {{/if}} - {{#if internalclass}} - <div class="{{internalclass}} icon-checkmark">{{internallabel}}</div> - {{/if}} <div class="app-detailpage"></div> <div class="app-description-container hidden"> @@ -95,6 +133,24 @@ <ul id="apps-categories"> </ul> + <div id="app-settings"> + <div id="app-settings-header"> + <button class="settings-button" data-apps-slide-toggle="#app-settings-content"></button> + </div> + + <div id="app-settings-content" class="apps-experimental"> + <input type="checkbox" id="enable-experimental-apps" <?php if($_['experimentalEnabled']) { print_unescaped('checked="checked"'); }?>> + <label for="enable-experimental-apps"><?php p($l->t('Enable experimental apps')) ?></label> + <p> + <small> + <?php p($l->t('Experimental apps are not checked for security ' . + 'issues, new or known to be unstable and under heavy ' . + 'development. Installing them can cause data loss or security ' . + 'breaches.')) ?> + </small> + </p> + </div> + </div> </div> <div id="app-content"> <div id="apps-list" class="icon-loading"></div> diff --git a/settings/templates/help.php b/settings/templates/help.php index f559329c6bb..79584aba84d 100644 --- a/settings/templates/help.php +++ b/settings/templates/help.php @@ -4,25 +4,25 @@ <li> <a class="<?php p($_['style1']); ?>" href="<?php print_unescaped($_['url1']); ?>"> - <?php p($l->t( 'User Documentation' )); ?> + <?php p($l->t('User documentation')); ?> </a> </li> <li> <a class="<?php p($_['style2']); ?>" href="<?php print_unescaped($_['url2']); ?>"> - <?php p($l->t( 'Administrator Documentation' )); ?> + <?php p($l->t('Administrator documentation')); ?> </a> </li> <?php } ?> <li> <a href="https://owncloud.org/support" target="_blank" rel="noreferrer"> - <?php p($l->t( 'Online Documentation' )); ?> ↗ + <?php p($l->t('Online documentation')); ?> ↗ </a> </li> <li> <a href="https://forum.owncloud.org" target="_blank" rel="noreferrer"> - <?php p($l->t( 'Forum' )); ?> ↗ + <?php p($l->t('Forum')); ?> ↗ </a> </li> @@ -30,14 +30,14 @@ <li> <a href="https://github.com/owncloud/core/blob/master/CONTRIBUTING.md" target="_blank" rel="noreferrer"> - <?php p($l->t( 'Bugtracker' )); ?> ↗ + <?php p($l->t('Issue tracker')); ?> ↗ </a> </li> <?php } ?> <li> - <a href="https://owncloud.com" target="_blank" rel="noreferrer"> - <?php p($l->t( 'Commercial Support' )); ?> ↗ + <a href="https://owncloud.com/subscriptions/" target="_blank" rel="noreferrer"> + <?php p($l->t('Commercial support')); ?> ↗ </a> </li> </div> diff --git a/settings/tests/js/appsSpec.js b/settings/tests/js/appsSpec.js index 39329c19246..311fade2c66 100644 --- a/settings/tests/js/appsSpec.js +++ b/settings/tests/js/appsSpec.js @@ -98,4 +98,58 @@ describe('OC.Settings.Apps tests', function() { expect(results[0]).toEqual('somestuff'); }); }); + + describe('loading categories', function() { + var suite = this; + + beforeEach( function(){ + suite.server = sinon.fakeServer.create(); + }); + + afterEach( function(){ + suite.server.restore(); + }); + + function getResultsFromDom() { + var results = []; + $('#apps-list .section:not(.hidden)').each(function() { + results.push($(this).attr('data-id')); + }); + return results; + } + + it('sorts all applications using the level', function() { + Apps.loadCategory('TestId'); + + suite.server.requests[0].respond( + 200, + { + 'Content-Type': 'application/json' + }, + JSON.stringify({ + apps: [ + { + id: 'foo', + level: 0 + }, + { + id: 'alpha', + level: 300 + }, + { + id: 'delta', + level: 200 + } + ] + }) + ); + + var results = getResultsFromDom(); + expect(results.length).toEqual(3); + expect(results[0]).toEqual('alpha'); + expect(results[1]).toEqual('delta'); + expect(results[2]).toEqual('foo'); + }); + }); + }); diff --git a/status.php b/status.php index 1628e824e00..6e7bcea5266 100644 --- a/status.php +++ b/status.php @@ -41,6 +41,7 @@ try { if (OC::$CLI) { print_r($values); } else { + header('Content-Type: application/json'); echo json_encode($values); } diff --git a/tests/lib/OCSClientTest.php b/tests/lib/OCSClientTest.php new file mode 100644 index 00000000000..fa3f1fe7848 --- /dev/null +++ b/tests/lib/OCSClientTest.php @@ -0,0 +1,934 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +use OC\OCSClient; +use OCP\Http\Client\IClientService; +use OCP\IConfig; +use OCP\ILogger; + +/** + * Class OCSClientTest + */ +class OCSClientTest extends \Test\TestCase { + /** @var OCSClient */ + private $ocsClient; + /** @var IConfig */ + private $config; + /** @var IClientService */ + private $clientService; + /** @var ILogger */ + private $logger; + + public function setUp() { + parent::setUp(); + + $this->config = $this->getMockBuilder('\OCP\IConfig') + ->disableOriginalConstructor()->getMock(); + $this->clientService = $this->getMock('\OCP\Http\Client\IClientService'); + $this->logger = $this->getMock('\OCP\ILogger'); + + $this->ocsClient = new OCSClient( + $this->clientService, + $this->config, + $this->logger + ); + } + + public function testIsAppStoreEnabledSuccess() { + $this->config + ->expects($this->once()) + ->method('getSystemValue') + ->with('appstoreenabled', true) + ->will($this->returnValue(true)); + $this->assertTrue($this->ocsClient->isAppStoreEnabled()); + } + + public function testIsAppStoreEnabledFail() { + $this->config + ->expects($this->once()) + ->method('getSystemValue') + ->with('appstoreenabled', true) + ->will($this->returnValue(false)); + $this->assertFalse($this->ocsClient->isAppStoreEnabled()); + } + + public function testGetAppStoreUrl() { + $this->config + ->expects($this->once()) + ->method('getSystemValue') + ->with('appstoreurl', 'https://api.owncloud.com/v1') + ->will($this->returnValue('https://api.owncloud.com/v1')); + $this->assertSame('https://api.owncloud.com/v1', Test_Helper::invokePrivate($this->ocsClient, 'getAppStoreUrl')); + } + + public function testGetCategoriesDisabledAppStore() { + $this->config + ->expects($this->once()) + ->method('getSystemValue') + ->with('appstoreenabled', true) + ->will($this->returnValue(false)); + $this->assertNull($this->ocsClient->getCategories()); + } + + public function testGetCategoriesExceptionClient() { + $this->config + ->expects($this->at(0)) + ->method('getSystemValue') + ->with('appstoreenabled', true) + ->will($this->returnValue(true)); + $this->config + ->expects($this->at(1)) + ->method('getSystemValue') + ->with('appstoreurl', 'https://api.owncloud.com/v1') + ->will($this->returnValue('https://api.owncloud.com/v1')); + + $client = $this->getMock('\OCP\Http\Client\IClient'); + $client + ->expects($this->once()) + ->method('get') + ->with( + 'https://api.owncloud.com/v1/content/categories', + [ + 'timeout' => 5, + ] + ) + ->will($this->throwException(new \Exception('TheErrorMessage'))); + + $this->clientService + ->expects($this->once()) + ->method('newClient') + ->will($this->returnValue($client)); + + $this->logger + ->expects($this->once()) + ->method('error') + ->with( + 'Could not get categories: TheErrorMessage', + [ + 'app' => 'core', + ] + ); + + $this->assertNull($this->ocsClient->getCategories()); + } + + public function testGetCategoriesParseError() { + $this->config + ->expects($this->at(0)) + ->method('getSystemValue') + ->with('appstoreenabled', true) + ->will($this->returnValue(true)); + $this->config + ->expects($this->at(1)) + ->method('getSystemValue') + ->with('appstoreurl', 'https://api.owncloud.com/v1') + ->will($this->returnValue('https://api.owncloud.com/v1')); + + $response = $this->getMock('\OCP\Http\Client\IResponse'); + $response + ->expects($this->once()) + ->method('getBody') + ->will($this->returnValue('MyInvalidXml')); + + $client = $this->getMock('\OCP\Http\Client\IClient'); + $client + ->expects($this->once()) + ->method('get') + ->with( + 'https://api.owncloud.com/v1/content/categories', + [ + 'timeout' => 5, + ] + ) + ->will($this->returnValue($response)); + + $this->clientService + ->expects($this->once()) + ->method('newClient') + ->will($this->returnValue($client)); + + $this->logger + ->expects($this->once()) + ->method('error') + ->with( + 'Could not get categories, content was no valid XML', + [ + 'app' => 'core', + ] + ); + + $this->assertNull($this->ocsClient->getCategories()); + } + + public function testGetCategoriesSuccessful() { + $this->config + ->expects($this->at(0)) + ->method('getSystemValue') + ->with('appstoreenabled', true) + ->will($this->returnValue(true)); + $this->config + ->expects($this->at(1)) + ->method('getSystemValue') + ->with('appstoreurl', 'https://api.owncloud.com/v1') + ->will($this->returnValue('https://api.owncloud.com/v1')); + + $response = $this->getMock('\OCP\Http\Client\IResponse'); + $response + ->expects($this->once()) + ->method('getBody') + ->will($this->returnValue('<?xml version="1.0"?> + <ocs> + <meta> + <status>ok</status> + <statuscode>100</statuscode> + <message></message> + <totalitems>6</totalitems> + </meta> + <data> + <category> + <id>920</id> + <name>ownCloud Multimedia</name> + </category> + <category> + <id>921</id> + <name>ownCloud PIM</name> + </category> + <category> + <id>922</id> + <name>ownCloud Productivity</name> + </category> + <category> + <id>923</id> + <name>ownCloud Game</name> + </category> + <category> + <id>924</id> + <name>ownCloud Tool</name> + </category> + <category> + <id>925</id> + <name>ownCloud other</name> + </category> + </data> + </ocs> + ')); + + $client = $this->getMock('\OCP\Http\Client\IClient'); + $client + ->expects($this->once()) + ->method('get') + ->with( + 'https://api.owncloud.com/v1/content/categories', + [ + 'timeout' => 5, + ] + ) + ->will($this->returnValue($response)); + + $this->clientService + ->expects($this->once()) + ->method('newClient') + ->will($this->returnValue($client)); + + $expected = [ + 920 => 'ownCloud Multimedia', + 921 => 'ownCloud PIM', + 922 => 'ownCloud Productivity', + 923 => 'ownCloud Game', + 924 => 'ownCloud Tool', + 925 => 'ownCloud other', + ]; + $this->assertSame($expected, $this->ocsClient->getCategories()); + } + + public function testGetApplicationsDisabledAppStore() { + $this->config + ->expects($this->once()) + ->method('getSystemValue') + ->with('appstoreenabled', true) + ->will($this->returnValue(false)); + $this->assertSame([], $this->ocsClient->getApplications([], 1, 'approved')); + } + + public function testGetApplicationsExceptionClient() { + $this->config + ->expects($this->at(0)) + ->method('getSystemValue') + ->with('appstoreenabled', true) + ->will($this->returnValue(true)); + $this->config + ->expects($this->at(1)) + ->method('getSystemValue') + ->with('appstoreurl', 'https://api.owncloud.com/v1') + ->will($this->returnValue('https://api.owncloud.com/v1')); + + $client = $this->getMock('\OCP\Http\Client\IClient'); + $client + ->expects($this->once()) + ->method('get') + ->with( + 'https://api.owncloud.com/v1/content/data', + [ + 'timeout' => 5, + 'query' => [ + 'version' => implode('x', \OC_Util::getVersion()), + 'filter' => 'approved', + 'categories' => '815x1337', + 'sortmode' => 'new', + 'page' => 1, + 'pagesize' => 100, + 'approved' => 'approved', + ], + ] + ) + ->will($this->throwException(new \Exception('TheErrorMessage'))); + + $this->clientService + ->expects($this->once()) + ->method('newClient') + ->will($this->returnValue($client)); + + $this->logger + ->expects($this->once()) + ->method('error') + ->with( + 'Could not get applications: TheErrorMessage', + [ + 'app' => 'core', + ] + ); + + $this->assertSame([], $this->ocsClient->getApplications([815, 1337], 1, 'approved')); + } + + public function testGetApplicationsParseError() { + $this->config + ->expects($this->at(0)) + ->method('getSystemValue') + ->with('appstoreenabled', true) + ->will($this->returnValue(true)); + $this->config + ->expects($this->at(1)) + ->method('getSystemValue') + ->with('appstoreurl', 'https://api.owncloud.com/v1') + ->will($this->returnValue('https://api.owncloud.com/v1')); + + $response = $this->getMock('\OCP\Http\Client\IResponse'); + $response + ->expects($this->once()) + ->method('getBody') + ->will($this->returnValue('MyInvalidXml')); + + $client = $this->getMock('\OCP\Http\Client\IClient'); + $client + ->expects($this->once()) + ->method('get') + ->with( + 'https://api.owncloud.com/v1/content/data', + [ + 'timeout' => 5, + 'query' => [ + 'version' => implode('x', \OC_Util::getVersion()), + 'filter' => 'approved', + 'categories' => '815x1337', + 'sortmode' => 'new', + 'page' => 1, + 'pagesize' => 100, + 'approved' => 'approved', + ], + ] + ) + ->will($this->returnValue($response)); + + $this->clientService + ->expects($this->once()) + ->method('newClient') + ->will($this->returnValue($client)); + + $this->logger + ->expects($this->once()) + ->method('error') + ->with( + 'Could not get applications, content was no valid XML', + [ + 'app' => 'core', + ] + ); + + $this->assertSame([], $this->ocsClient->getApplications([815, 1337], 1, 'approved')); + } + + public function testGetApplicationsSuccessful() { + $this->config + ->expects($this->at(0)) + ->method('getSystemValue') + ->with('appstoreenabled', true) + ->will($this->returnValue(true)); + $this->config + ->expects($this->at(1)) + ->method('getSystemValue') + ->with('appstoreurl', 'https://api.owncloud.com/v1') + ->will($this->returnValue('https://api.owncloud.com/v1')); + + $response = $this->getMock('\OCP\Http\Client\IResponse'); + $response + ->expects($this->once()) + ->method('getBody') + ->will($this->returnValue('<?xml version="1.0"?> + <ocs> + <meta> + <status>ok</status> + <statuscode>100</statuscode> + <message></message> + <totalitems>2</totalitems> + <itemsperpage>100</itemsperpage> + </meta> + <data> + <content details="summary"> + <id>168707</id> + <name>Calendar 8.0</name> + <version>0.6.4</version> + <label>recommended</label> + <changed>2015-02-09T15:23:56+01:00</changed> + <created>2015-01-26T04:35:19+01:00</created> + <typeid>921</typeid> + <typename>ownCloud PIM</typename> + <language></language> + <personid>owncloud</personid> + <profilepage>http://opendesktop.org/usermanager/search.php?username=owncloud</profilepage> + <downloads>5393</downloads> + <score>60</score> + <description>Calendar App for ownCloud</description> + <comments>7</comments> + <fans>10</fans> + <licensetype>16</licensetype> + <approved>0</approved> + <category>1</category> + <license>AGPL</license> + <preview1></preview1> + <detailpage>https://apps.owncloud.com/content/show.php?content=168707</detailpage> + <downloadtype1></downloadtype1> + <downloadway1>0</downloadway1> + <downloadprice1>0</downloadprice1> + <downloadlink1>http://apps.owncloud.com/content/download.php?content=168707&id=1</downloadlink1> + <downloadgpgsignature1></downloadgpgsignature1> + <downloadgpgfingerprint1></downloadgpgfingerprint1> + <downloadpackagename1></downloadpackagename1> + <downloadrepository1></downloadrepository1> + <downloadname1></downloadname1> + <downloadsize1>885</downloadsize1> + </content> + <content details="summary"> + <id>168708</id> + <name>Contacts 8.0</name> + <version>0.3.0.18</version> + <label>recommended</label> + <changed>2015-02-09T15:18:58+01:00</changed> + <created>2015-01-26T04:45:17+01:00</created> + <typeid>921</typeid> + <typename>ownCloud PIM</typename> + <language></language> + <personid>owncloud</personid> + <profilepage>http://opendesktop.org/usermanager/search.php?username=owncloud</profilepage> + <downloads>4237</downloads> + <score>58</score> + <description></description> + <comments>3</comments> + <fans>6</fans> + <licensetype>16</licensetype> + <approved>200</approved> + <category>1</category> + <license>AGPL</license> + <preview1></preview1> + <detailpage>https://apps.owncloud.com/content/show.php?content=168708</detailpage> + <downloadtype1></downloadtype1> + <downloadway1>0</downloadway1> + <downloadprice1>0</downloadprice1> + <downloadlink1>http://apps.owncloud.com/content/download.php?content=168708&id=1</downloadlink1> + <downloadgpgsignature1></downloadgpgsignature1> + <downloadgpgfingerprint1></downloadgpgfingerprint1> + <downloadpackagename1></downloadpackagename1> + <downloadrepository1></downloadrepository1> + <downloadname1></downloadname1> + <downloadsize1>1409</downloadsize1> + </content> + </data> + </ocs> ')); + + $client = $this->getMock('\OCP\Http\Client\IClient'); + $client + ->expects($this->once()) + ->method('get') + ->with( + 'https://api.owncloud.com/v1/content/data', + [ + 'timeout' => 5, + 'query' => [ + 'version' => implode('x', \OC_Util::getVersion()), + 'filter' => 'approved', + 'categories' => '815x1337', + 'sortmode' => 'new', + 'page' => 1, + 'pagesize' => 100, + 'approved' => 'approved', + ], + ] + ) + ->will($this->returnValue($response)); + + $this->clientService + ->expects($this->once()) + ->method('newClient') + ->will($this->returnValue($client)); + + $expected = [ + [ + 'id' => '168707', + 'name' => 'Calendar 8.0', + 'label' => 'recommended', + 'version' => '0.6.4', + 'type' => '921', + 'typename' => 'ownCloud PIM', + 'personid' => 'owncloud', + 'license' => 'AGPL', + 'detailpage' => 'https://apps.owncloud.com/content/show.php?content=168707', + 'preview' => '', + 'preview-full' => '', + 'changed' => 1423491836, + 'description' => 'Calendar App for ownCloud', + 'score' => '60', + 'downloads' => 5393, + 'level' => 0, + 'profilepage' => 'http://opendesktop.org/usermanager/search.php?username=owncloud', + ], + [ + 'id' => '168708', + 'name' => 'Contacts 8.0', + 'label' => 'recommended', + 'version' => '0.3.0.18', + 'type' => '921', + 'typename' => 'ownCloud PIM', + 'personid' => 'owncloud', + 'license' => 'AGPL', + 'detailpage' => 'https://apps.owncloud.com/content/show.php?content=168708', + 'preview' => '', + 'preview-full' => '', + 'changed' => 1423491538, + 'description' => '', + 'score' => '58', + 'downloads' => 4237, + 'level' => 200, + 'profilepage' => 'http://opendesktop.org/usermanager/search.php?username=owncloud', + ], + ]; + $this->assertEquals($expected, $this->ocsClient->getApplications([815, 1337], 1, 'approved')); + } + + public function tesGetApplicationDisabledAppStore() { + $this->config + ->expects($this->once()) + ->method('getSystemValue') + ->with('appstoreenabled', true) + ->will($this->returnValue(false)); + $this->assertNull($this->ocsClient->getApplication('MyId')); + } + + public function testGetApplicationExceptionClient() { + $this->config + ->expects($this->at(0)) + ->method('getSystemValue') + ->with('appstoreenabled', true) + ->will($this->returnValue(true)); + $this->config + ->expects($this->at(1)) + ->method('getSystemValue') + ->with('appstoreurl', 'https://api.owncloud.com/v1') + ->will($this->returnValue('https://api.owncloud.com/v1')); + + $client = $this->getMock('\OCP\Http\Client\IClient'); + $client + ->expects($this->once()) + ->method('get') + ->with( + 'https://api.owncloud.com/v1/content/data/MyId', + [ + 'timeout' => 5, + ] + ) + ->will($this->throwException(new \Exception('TheErrorMessage'))); + + $this->clientService + ->expects($this->once()) + ->method('newClient') + ->will($this->returnValue($client)); + + $this->logger + ->expects($this->once()) + ->method('error') + ->with( + 'Could not get application: TheErrorMessage', + [ + 'app' => 'core', + ] + ); + + $this->assertNull($this->ocsClient->getApplication('MyId')); + } + + public function testGetApplicationParseError() { + $this->config + ->expects($this->at(0)) + ->method('getSystemValue') + ->with('appstoreenabled', true) + ->will($this->returnValue(true)); + $this->config + ->expects($this->at(1)) + ->method('getSystemValue') + ->with('appstoreurl', 'https://api.owncloud.com/v1') + ->will($this->returnValue('https://api.owncloud.com/v1')); + + $response = $this->getMock('\OCP\Http\Client\IResponse'); + $response + ->expects($this->once()) + ->method('getBody') + ->will($this->returnValue('MyInvalidXml')); + + $client = $this->getMock('\OCP\Http\Client\IClient'); + $client + ->expects($this->once()) + ->method('get') + ->with( + 'https://api.owncloud.com/v1/content/data/MyId', + [ + 'timeout' => 5, + ] + ) + ->will($this->returnValue($response)); + + $this->clientService + ->expects($this->once()) + ->method('newClient') + ->will($this->returnValue($client)); + + $this->logger + ->expects($this->once()) + ->method('error') + ->with( + 'Could not get application, content was no valid XML', + [ + 'app' => 'core', + ] + ); + + $this->assertNull($this->ocsClient->getApplication('MyId')); + } + + public function testGetApplicationSuccessful() { + $this->config + ->expects($this->at(0)) + ->method('getSystemValue') + ->with('appstoreenabled', true) + ->will($this->returnValue(true)); + $this->config + ->expects($this->at(1)) + ->method('getSystemValue') + ->with('appstoreurl', 'https://api.owncloud.com/v1') + ->will($this->returnValue('https://api.owncloud.com/v1')); + + $response = $this->getMock('\OCP\Http\Client\IResponse'); + $response + ->expects($this->once()) + ->method('getBody') + ->will($this->returnValue('<?xml version="1.0"?> + <ocs> + <meta> + <status>ok</status> + <statuscode>100</statuscode> + <message></message> + </meta> + <data> + <content details="full"> + <id>166053</id> + <name>Versioning</name> + <version>0.0.1</version> + <label>recommended</label> + <typeid>925</typeid> + <typename>ownCloud other</typename> + <language></language> + <personid>owncloud</personid> + <profilepage>http://opendesktop.org/usermanager/search.php?username=owncloud</profilepage> + <created>2014-07-07T16:34:40+02:00</created> + <changed>2014-07-07T16:34:40+02:00</changed> + <downloads>140</downloads> + <score>50</score> + <description>Placeholder for future updates</description> + <summary></summary> + <feedbackurl></feedbackurl> + <changelog></changelog> + <homepage></homepage> + <homepagetype></homepagetype> + <homepage2></homepage2> + <homepagetype2></homepagetype2> + <homepage3></homepage3> + <homepagetype3></homepagetype3> + <homepage4></homepage4> + <homepagetype4></homepagetype4> + <homepage5></homepage5> + <homepagetype5></homepagetype5> + <homepage6></homepage6> + <homepagetype6></homepagetype6> + <homepage7></homepage7> + <homepagetype7></homepagetype7> + <homepage8></homepage8> + <homepagetype8></homepagetype8> + <homepage9></homepage9> + <homepagetype9></homepagetype9> + <homepage10></homepage10> + <homepagetype10></homepagetype10> + <licensetype>16</licensetype> + <license>AGPL</license> + <donationpage></donationpage> + <comments>0</comments> + <commentspage>http://apps.owncloud.com/content/show.php?content=166053</commentspage> + <fans>0</fans> + <fanspage>http://apps.owncloud.com/content/show.php?action=fan&content=166053</fanspage> + <knowledgebaseentries>0</knowledgebaseentries> + <knowledgebasepage>http://apps.owncloud.com/content/show.php?action=knowledgebase&content=166053</knowledgebasepage> + <depend>ownCloud 7</depend> + <preview1></preview1> + <preview2></preview2> + <preview3></preview3> + <previewpic1></previewpic1> + <previewpic2></previewpic2> + <previewpic3></previewpic3> + <picsmall1></picsmall1> + <picsmall2></picsmall2> + <picsmall3></picsmall3> + <detailpage>https://apps.owncloud.com/content/show.php?content=166053</detailpage> + <downloadtype1></downloadtype1> + <downloadprice1>0</downloadprice1> + <downloadlink1>http://apps.owncloud.com/content/download.php?content=166053&id=1</downloadlink1> + <downloadname1></downloadname1> + <downloadgpgfingerprint1></downloadgpgfingerprint1> + <downloadgpgsignature1></downloadgpgsignature1> + <downloadpackagename1></downloadpackagename1> + <downloadrepository1></downloadrepository1> + <downloadsize1>1</downloadsize1> + </content> + </data> + </ocs> + ')); + + $client = $this->getMock('\OCP\Http\Client\IClient'); + $client + ->expects($this->once()) + ->method('get') + ->with( + 'https://api.owncloud.com/v1/content/data/MyId', + [ + 'timeout' => 5, + ] + ) + ->will($this->returnValue($response)); + + $this->clientService + ->expects($this->once()) + ->method('newClient') + ->will($this->returnValue($client)); + + $expected = [ + 'id' => 166053, + 'name' => 'Versioning', + 'version' => '0.0.1', + 'type' => '925', + 'label' => 'recommended', + 'typename' => 'ownCloud other', + 'personid' => 'owncloud', + 'profilepage' => 'http://opendesktop.org/usermanager/search.php?username=owncloud', + 'detailpage' => 'https://apps.owncloud.com/content/show.php?content=166053', + 'preview1' => '', + 'preview2' => '', + 'preview3' => '', + 'changed' => 1404743680, + 'description' => 'Placeholder for future updates', + 'score' => 50, + ]; + $this->assertSame($expected, $this->ocsClient->getApplication('MyId')); + } + + public function testGetApplicationDownloadDisabledAppStore() { + $this->config + ->expects($this->once()) + ->method('getSystemValue') + ->with('appstoreenabled', true) + ->will($this->returnValue(false)); + $this->assertNull($this->ocsClient->getApplicationDownload('MyId')); + } + + public function testGetApplicationDownloadExceptionClient() { + $this->config + ->expects($this->at(0)) + ->method('getSystemValue') + ->with('appstoreenabled', true) + ->will($this->returnValue(true)); + $this->config + ->expects($this->at(1)) + ->method('getSystemValue') + ->with('appstoreurl', 'https://api.owncloud.com/v1') + ->will($this->returnValue('https://api.owncloud.com/v1')); + + $client = $this->getMock('\OCP\Http\Client\IClient'); + $client + ->expects($this->once()) + ->method('get') + ->with( + 'https://api.owncloud.com/v1/content/download/MyId/1', + [ + 'timeout' => 5, + ] + ) + ->will($this->throwException(new \Exception('TheErrorMessage'))); + + $this->clientService + ->expects($this->once()) + ->method('newClient') + ->will($this->returnValue($client)); + + $this->logger + ->expects($this->once()) + ->method('error') + ->with( + 'Could not get application download URL: TheErrorMessage', + [ + 'app' => 'core', + ] + ); + + $this->assertNull($this->ocsClient->getApplicationDownload('MyId')); + } + + public function testGetApplicationDownloadParseError() { + $this->config + ->expects($this->at(0)) + ->method('getSystemValue') + ->with('appstoreenabled', true) + ->will($this->returnValue(true)); + $this->config + ->expects($this->at(1)) + ->method('getSystemValue') + ->with('appstoreurl', 'https://api.owncloud.com/v1') + ->will($this->returnValue('https://api.owncloud.com/v1')); + + $response = $this->getMock('\OCP\Http\Client\IResponse'); + $response + ->expects($this->once()) + ->method('getBody') + ->will($this->returnValue('MyInvalidXml')); + + $client = $this->getMock('\OCP\Http\Client\IClient'); + $client + ->expects($this->once()) + ->method('get') + ->with( + 'https://api.owncloud.com/v1/content/download/MyId/1', + [ + 'timeout' => 5, + ] + ) + ->will($this->returnValue($response)); + + $this->clientService + ->expects($this->once()) + ->method('newClient') + ->will($this->returnValue($client)); + + $this->logger + ->expects($this->once()) + ->method('error') + ->with( + 'Could not get application download URL, content was no valid XML', + [ + 'app' => 'core', + ] + ); + + $this->assertNull($this->ocsClient->getApplicationDownload('MyId')); + } + + public function testGetApplicationDownloadUrlSuccessful() { + $this->config + ->expects($this->at(0)) + ->method('getSystemValue') + ->with('appstoreenabled', true) + ->will($this->returnValue(true)); + $this->config + ->expects($this->at(1)) + ->method('getSystemValue') + ->with('appstoreurl', 'https://api.owncloud.com/v1') + ->will($this->returnValue('https://api.owncloud.com/v1')); + + $response = $this->getMock('\OCP\Http\Client\IResponse'); + $response + ->expects($this->once()) + ->method('getBody') + ->will($this->returnValue('<?xml version="1.0"?> + <ocs> + <meta> + <status>ok</status> + <statuscode>100</statuscode> + <message></message> + </meta> + <data> + <content details="download"> + <downloadlink>https://apps.owncloud.com/CONTENT/content-files/166052-files_trashbin.zip</downloadlink> + <mimetype>application/zip</mimetype> + <gpgfingerprint></gpgfingerprint> + <gpgsignature></gpgsignature> + <packagename></packagename> + <repository></repository> + </content> + </data> + </ocs> + ')); + + $client = $this->getMock('\OCP\Http\Client\IClient'); + $client + ->expects($this->once()) + ->method('get') + ->with( + 'https://api.owncloud.com/v1/content/download/MyId/1', + [ + 'timeout' => 5, + ] + ) + ->will($this->returnValue($response)); + + $this->clientService + ->expects($this->once()) + ->method('newClient') + ->will($this->returnValue($client)); + + $expected = [ + 'downloadlink' => 'https://apps.owncloud.com/CONTENT/content-files/166052-files_trashbin.zip', + ]; + $this->assertSame($expected, $this->ocsClient->getApplicationDownload('MyId')); + } +} diff --git a/tests/lib/activitymanager.php b/tests/lib/activitymanager.php index d227c05d827..d3263fa2ede 100644 --- a/tests/lib/activitymanager.php +++ b/tests/lib/activitymanager.php @@ -13,10 +13,34 @@ class Test_ActivityManager extends \Test\TestCase { /** @var \OC\ActivityManager */ private $activityManager; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $request; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $session; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $config; + protected function setUp() { parent::setUp(); - $this->activityManager = new \OC\ActivityManager(); + $this->request = $this->getMockBuilder('OCP\IRequest') + ->disableOriginalConstructor() + ->getMock(); + $this->session = $this->getMockBuilder('OCP\IUserSession') + ->disableOriginalConstructor() + ->getMock(); + $this->config = $this->getMockBuilder('OCP\IConfig') + ->disableOriginalConstructor() + ->getMock(); + + $this->activityManager = new \OC\ActivityManager( + $this->request, + $this->session, + $this->config + ); + $this->activityManager->registerExtension(function() { return new NoOpExtension(); }); @@ -111,6 +135,83 @@ class Test_ActivityManager extends \Test\TestCase { $result = $this->activityManager->getQueryForFilter('InvalidFilter'); $this->assertEquals(array(null, null), $result); } + + public function getUserFromTokenThrowInvalidTokenData() { + return [ + [null, []], + ['', []], + ['12345678901234567890123456789', []], + ['1234567890123456789012345678901', []], + ['123456789012345678901234567890', []], + ['123456789012345678901234567890', ['user1', 'user2']], + ]; + } + + /** + * @expectedException \UnexpectedValueException + * @dataProvider getUserFromTokenThrowInvalidTokenData + * + * @param string $token + * @param array $users + */ + public function testGetUserFromTokenThrowInvalidToken($token, $users) { + $this->mockRSSToken($token, $token, $users); + \Test_Helper::invokePrivate($this->activityManager, 'getUserFromToken'); + } + + public function getUserFromTokenData() { + return [ + [null, '123456789012345678901234567890', 'user1'], + ['user2', null, 'user2'], + ['user2', '123456789012345678901234567890', 'user2'], + ]; + } + + /** + * @dataProvider getUserFromTokenData + * + * @param string $userLoggedIn + * @param string $token + * @param string $expected + */ + public function testGetUserFromToken($userLoggedIn, $token, $expected) { + if ($userLoggedIn !== null) { + $this->mockUserSession($userLoggedIn); + } + $this->mockRSSToken($token, '123456789012345678901234567890', ['user1']); + + $this->assertEquals($expected, $this->activityManager->getCurrentUserId()); + } + + protected function mockRSSToken($requestToken, $userToken, $users) { + if ($requestToken !== null) { + $this->request->expects($this->any()) + ->method('getParam') + ->with('token', '') + ->willReturn($requestToken); + } + + $this->config->expects($this->any()) + ->method('getUsersForUserValue') + ->with('activity', 'rsstoken', $userToken) + ->willReturn($users); + } + + protected function mockUserSession($user) { + $mockUser = $this->getMockBuilder('\OCP\IUser') + ->disableOriginalConstructor() + ->getMock(); + $mockUser->expects($this->any()) + ->method('getUID') + ->willReturn($user); + + $this->session->expects($this->any()) + ->method('isLoggedIn') + ->willReturn(true); + $this->session->expects($this->any()) + ->method('getUser') + ->willReturn($mockUser); + } } class SimpleExtension implements \OCP\Activity\IExtension { diff --git a/tests/lib/encryption/keys/storage.php b/tests/lib/encryption/keys/storage.php index 8ab46987f8c..bcf1c0f7624 100644 --- a/tests/lib/encryption/keys/storage.php +++ b/tests/lib/encryption/keys/storage.php @@ -198,6 +198,10 @@ class StorageTest extends TestCase { public function testDeleteUserKey() { $this->view->expects($this->once()) + ->method('file_exists') + ->with($this->equalTo('/user1/files_encryption/encModule/user1.publicKey')) + ->willReturn(true); + $this->view->expects($this->once()) ->method('unlink') ->with($this->equalTo('/user1/files_encryption/encModule/user1.publicKey')) ->willReturn(true); @@ -209,6 +213,10 @@ class StorageTest extends TestCase { public function testDeleteSystemUserKey() { $this->view->expects($this->once()) + ->method('file_exists') + ->with($this->equalTo('/files_encryption/encModule/shareKey_56884')) + ->willReturn(true); + $this->view->expects($this->once()) ->method('unlink') ->with($this->equalTo('/files_encryption/encModule/shareKey_56884')) ->willReturn(true); @@ -229,6 +237,10 @@ class StorageTest extends TestCase { ->method('isSystemWideMountPoint') ->willReturn(true); $this->view->expects($this->once()) + ->method('file_exists') + ->with($this->equalTo('/files_encryption/keys/files/foo.txt/encModule/fileKey')) + ->willReturn(true); + $this->view->expects($this->once()) ->method('unlink') ->with($this->equalTo('/files_encryption/keys/files/foo.txt/encModule/fileKey')) ->willReturn(true); @@ -249,6 +261,10 @@ class StorageTest extends TestCase { ->method('isSystemWideMountPoint') ->willReturn(false); $this->view->expects($this->once()) + ->method('file_exists') + ->with($this->equalTo('/user1/files_encryption/keys/files/foo.txt/encModule/fileKey')) + ->willReturn(true); + $this->view->expects($this->once()) ->method('unlink') ->with($this->equalTo('/user1/files_encryption/keys/files/foo.txt/encModule/fileKey')) ->willReturn(true); diff --git a/tests/lib/files/cache/updater.php b/tests/lib/files/cache/updater.php index 970af2e68df..7c3ebd5a6f9 100644 --- a/tests/lib/files/cache/updater.php +++ b/tests/lib/files/cache/updater.php @@ -33,16 +33,12 @@ class Updater extends \Test\TestCase { */ protected $updater; - /** @var \OC\Files\Storage\Storage */ - private $originalStorage; - protected function setUp() { parent::setUp(); - $this->originalStorage = Filesystem::getStorage('/'); + $this->loginAsUser(); $this->storage = new Temporary(array()); - Filesystem::clearMounts(); Filesystem::mount($this->storage, array(), '/'); $this->view = new View(''); $this->updater = new \OC\Files\Cache\Updater($this->view); @@ -51,8 +47,8 @@ class Updater extends \Test\TestCase { protected function tearDown() { Filesystem::clearMounts(); - Filesystem::mount($this->originalStorage, array(), '/'); + $this->logout(); parent::tearDown(); } diff --git a/tests/lib/files/cache/updaterlegacy.php b/tests/lib/files/cache/updaterlegacy.php index 6bdacbe34fe..f4d52e9a390 100644 --- a/tests/lib/files/cache/updaterlegacy.php +++ b/tests/lib/files/cache/updaterlegacy.php @@ -27,9 +27,6 @@ class UpdaterLegacy extends \Test\TestCase { */ private $cache; - /** @var \OC\Files\Storage\Storage */ - private $originalStorage; - private static $user; protected function setUp() { @@ -48,14 +45,12 @@ class UpdaterLegacy extends \Test\TestCase { $this->scanner->scan(''); $this->cache = $this->storage->getCache(); - $this->originalStorage = Filesystem::getStorage('/'); - Filesystem::tearDown(); if (!self::$user) { self::$user = $this->getUniqueID(); } \OC_User::createUser(self::$user, 'password'); - \OC_User::setUserId(self::$user); + $this->loginAsUser(self::$user); Filesystem::init(self::$user, '/' . self::$user . '/files'); @@ -71,9 +66,8 @@ class UpdaterLegacy extends \Test\TestCase { } $result = \OC_User::deleteUser(self::$user); $this->assertTrue($result); - Filesystem::tearDown(); - Filesystem::mount($this->originalStorage, array(), '/'); + $this->logout(); parent::tearDown(); } diff --git a/tests/lib/files/cache/watcher.php b/tests/lib/files/cache/watcher.php index ee605c64e01..e6947e36a17 100644 --- a/tests/lib/files/cache/watcher.php +++ b/tests/lib/files/cache/watcher.php @@ -15,14 +15,10 @@ class Watcher extends \Test\TestCase { */ private $storages = array(); - /** @var \OC\Files\Storage\Storage */ - private $originalStorage; - protected function setUp() { parent::setUp(); - $this->originalStorage = \OC\Files\Filesystem::getStorage('/'); - \OC\Files\Filesystem::clearMounts(); + $this->loginAsUser(); } protected function tearDown() { @@ -32,9 +28,7 @@ class Watcher extends \Test\TestCase { $cache->clear(); } - \OC\Files\Filesystem::clearMounts(); - \OC\Files\Filesystem::mount($this->originalStorage, array(), '/'); - + $this->logout(); parent::tearDown(); } diff --git a/tests/lib/files/etagtest.php b/tests/lib/files/etagtest.php index eec24d9f4c6..055927652bc 100644 --- a/tests/lib/files/etagtest.php +++ b/tests/lib/files/etagtest.php @@ -16,16 +16,11 @@ class EtagTest extends \Test\TestCase { private $tmpDir; - private $uid; - /** * @var \OC_User_Dummy $userBackend */ private $userBackend; - /** @var \OC\Files\Storage\Storage */ - private $originalStorage; - protected function setUp() { parent::setUp(); @@ -37,21 +32,15 @@ class EtagTest extends \Test\TestCase { $this->datadir = \OC_Config::getValue('datadirectory'); $this->tmpDir = \OC_Helper::tmpFolder(); \OC_Config::setValue('datadirectory', $this->tmpDir); - $this->uid = \OC_User::getUser(); - \OC_User::setUserId(null); $this->userBackend = new \OC_User_Dummy(); \OC_User::useBackend($this->userBackend); - $this->originalStorage = \OC\Files\Filesystem::getStorage('/'); - \OC_Util::tearDownFS(); } protected function tearDown() { \OC_Config::setValue('datadirectory', $this->datadir); - \OC_User::setUserId($this->uid); - \OC_Util::setupFS($this->uid); - \OC\Files\Filesystem::mount($this->originalStorage, array(), '/'); + $this->logout(); parent::tearDown(); } @@ -59,9 +48,7 @@ class EtagTest extends \Test\TestCase { $user1 = $this->getUniqueID('user_'); $this->userBackend->createUser($user1, ''); - \OC_Util::tearDownFS(); - \OC_User::setUserId($user1); - \OC_Util::setupFS($user1); + $this->loginAsUser($user1); Filesystem::mkdir('/folder'); Filesystem::mkdir('/folder/subfolder'); Filesystem::file_put_contents('/foo.txt', 'asd'); diff --git a/tests/lib/files/filesystem.php b/tests/lib/files/filesystem.php index 7bf59315d77..98e96e0cc78 100644 --- a/tests/lib/files/filesystem.php +++ b/tests/lib/files/filesystem.php @@ -28,9 +28,6 @@ class Filesystem extends \Test\TestCase { */ private $tmpDirs = array(); - /** @var \OC\Files\Storage\Storage */ - private $originalStorage; - /** * @return array */ @@ -42,20 +39,15 @@ class Filesystem extends \Test\TestCase { protected function setUp() { parent::setUp(); - - $this->originalStorage = \OC\Files\Filesystem::getStorage('/'); - \OC_User::setUserId(''); - \OC\Files\Filesystem::clearMounts(); + $this->loginAsUser(); } protected function tearDown() { foreach ($this->tmpDirs as $dir) { \OC_Helper::rmdirr($dir); } - \OC\Files\Filesystem::clearMounts(); - \OC\Files\Filesystem::mount($this->originalStorage, array(), '/'); - \OC_User::setUserId(''); + $this->logout(); parent::tearDown(); } diff --git a/tests/lib/files/node/integration.php b/tests/lib/files/node/integration.php index 456a4a0e287..4e362607240 100644 --- a/tests/lib/files/node/integration.php +++ b/tests/lib/files/node/integration.php @@ -20,9 +20,6 @@ class IntegrationTests extends \Test\TestCase { */ private $root; - /** @var \OC\Files\Storage\Storage */ - private $originalStorage; - /** * @var \OC\Files\Storage\Storage[] */ @@ -36,9 +33,6 @@ class IntegrationTests extends \Test\TestCase { protected function setUp() { parent::setUp(); - $this->originalStorage = \OC\Files\Filesystem::getStorage('/'); - \OC\Files\Filesystem::init('', ''); - \OC\Files\Filesystem::clearMounts(); $manager = \OC\Files\Filesystem::getMountManager(); \OC_Hook::clear('OC_Filesystem'); @@ -49,7 +43,8 @@ class IntegrationTests extends \Test\TestCase { \OC_Hook::connect('OC_Filesystem', 'post_touch', '\OC\Files\Cache\Updater', 'touchHook'); $user = new User($this->getUniqueID('user'), new \OC_User_Dummy); - \OC_User::setUserId($user->getUID()); + $this->loginAsUser($user->getUID()); + $this->view = new View(); $this->root = new Root($manager, $this->view, $user); $storage = new Temporary(array()); @@ -64,9 +59,8 @@ class IntegrationTests extends \Test\TestCase { foreach ($this->storages as $storage) { $storage->getCache()->clear(); } - \OC\Files\Filesystem::clearMounts(); - \OC\Files\Filesystem::mount($this->originalStorage, array(), '/'); + $this->logout(); parent::tearDown(); } diff --git a/tests/lib/files/storage/wrapper/encryption.php b/tests/lib/files/storage/wrapper/encryption.php index bf4464f0eb9..4f7a9e851c1 100644 --- a/tests/lib/files/storage/wrapper/encryption.php +++ b/tests/lib/files/storage/wrapper/encryption.php @@ -44,7 +44,9 @@ class Encryption extends \Test\Files\Storage\Storage { $file = $this->getMockBuilder('\OC\Encryption\File') ->disableOriginalConstructor() + ->setMethods(['getAccessList']) ->getMock(); + $file->expects($this->any())->method('getAccessList')->willReturn([]); $logger = $this->getMock('\OC\Log'); diff --git a/tests/lib/files/stream/encryption.php b/tests/lib/files/stream/encryption.php index 84156337ad7..53727a2213d 100644 --- a/tests/lib/files/stream/encryption.php +++ b/tests/lib/files/stream/encryption.php @@ -29,7 +29,9 @@ class Encryption extends \Test\TestCase { ->getMock(); $file = $this->getMockBuilder('\OC\Encryption\File') ->disableOriginalConstructor() + ->setMethods(['getAccessList']) ->getMock(); + $file->expects($this->any())->method('getAccessList')->willReturn([]); $util = $this->getMock('\OC\Encryption\Util', ['getUidAndFilename'], [new View(), new \OC\User\Manager(), $config]); $util->expects($this->any()) ->method('getUidAndFilename') diff --git a/tests/lib/files/utils/scanner.php b/tests/lib/files/utils/scanner.php index 65ddfe47514..dfc683c1bcf 100644 --- a/tests/lib/files/utils/scanner.php +++ b/tests/lib/files/utils/scanner.php @@ -39,18 +39,14 @@ class TestScanner extends \OC\Files\Utils\Scanner { } class Scanner extends \Test\TestCase { - /** @var \OC\Files\Storage\Storage */ - private $originalStorage; - protected function setUp() { parent::setUp(); - $this->originalStorage = \OC\Files\Filesystem::getStorage('/'); + $this->loginAsUser(); } protected function tearDown() { - \OC\Files\Filesystem::mount($this->originalStorage, array(), '/'); - + $this->logout(); parent::tearDown(); } diff --git a/tests/lib/files/view.php b/tests/lib/files/view.php index cd9f2d4afd1..2ea9e8de78f 100644 --- a/tests/lib/files/view.php +++ b/tests/lib/files/view.php @@ -27,9 +27,6 @@ class View extends \Test\TestCase { /** @var \OC\Files\Storage\Storage */ private $tempStorage; - /** @var \OC\Files\Storage\Storage */ - private $originalStorage; - protected function setUp() { parent::setUp(); @@ -39,9 +36,10 @@ class View extends \Test\TestCase { //login \OC_User::createUser('test', 'test'); $this->user = \OC_User::getUser(); - \OC_User::setUserId('test'); - $this->originalStorage = \OC\Files\Filesystem::getStorage('/'); + $this->loginAsUser('test'); + // clear mounts but somehow keep the root storage + // that was initialized above... \OC\Files\Filesystem::clearMounts(); $this->tempStorage = null; @@ -59,9 +57,7 @@ class View extends \Test\TestCase { system('rm -rf ' . escapeshellarg($this->tempStorage->getDataDir())); } - \OC\Files\Filesystem::clearMounts(); - \OC\Files\Filesystem::mount($this->originalStorage, array(), '/'); - + $this->logout(); parent::tearDown(); } diff --git a/tests/lib/preview.php b/tests/lib/preview.php index 2a6761403f4..ea9de9b777e 100644 --- a/tests/lib/preview.php +++ b/tests/lib/preview.php @@ -26,6 +26,8 @@ class Preview extends TestCase { protected function setUp() { parent::setUp(); + // FIXME: use proper tearDown with $this->loginAsUser() and $this->logout() + // (would currently break the tests for some reason) $this->originalStorage = \OC\Files\Filesystem::getStorage('/'); // create a new user with his own filesystem view @@ -48,6 +50,77 @@ class Preview extends TestCase { parent::tearDown(); } + public function testIsMaxSizeWorking() { + // Max size from config + $maxX = 1024; + $maxY = 1024; + + \OC::$server->getConfig()->setSystemValue('preview_max_x', $maxX); + \OC::$server->getConfig()->setSystemValue('preview_max_y', $maxY); + + // Sample is 1680x1050 JPEG + $sampleFile = '/' . $this->user . '/files/testimage.jpg'; + $this->rootView->file_put_contents($sampleFile, file_get_contents(\OC::$SERVERROOT.'/tests/data/testimage.jpg')); + $fileInfo = $this->rootView->getFileInfo($sampleFile); + $fileId = $fileInfo['fileid']; + + $largeX = 1920; + $largeY = 1080; + $preview = new \OC\Preview($this->user, 'files/', 'testimage.jpg', $largeX, $largeY); + + $this->assertEquals($preview->isFileValid(), true); + + // There should be no cached copy + $isCached = $preview->isCached($fileId); + + $this->assertNotEquals(\OC\Preview::THUMBNAILS_FOLDER . '/' . $fileId . '/' . $maxX . '-' . $maxY . '-max.png', $isCached); + $this->assertNotEquals(\OC\Preview::THUMBNAILS_FOLDER . '/' . $fileId . '/' . $maxX . '-' . $maxY . '.png', $isCached); + $this->assertNotEquals(\OC\Preview::THUMBNAILS_FOLDER . '/' . $fileId . '/' . $largeX . '-' . $largeY . '.png', $isCached); + + // The returned preview should be of max size + $image = $preview->getPreview(); + + $this->assertEquals($image->width(), $maxX); + $this->assertEquals($image->height(), $maxY); + + // The max thumbnail should be created + $maxThumbCacheFile = '/' . $this->user . '/' . \OC\Preview::THUMBNAILS_FOLDER . '/' . $fileId . '/' . $maxX . '-' . $maxY . '-max.png'; + + $this->assertEquals($this->rootView->file_exists($maxThumbCacheFile), true); + + // A preview of the asked size should not have been created + $thumbCacheFile = \OC\Preview::THUMBNAILS_FOLDER . '/' . $fileId . '/' . $largeX . '-' . $largeY . '.png'; + + $this->assertEquals($this->rootView->file_exists($thumbCacheFile), false); + + // 2nd request should indicate that we have a cached copy of max dimension + $isCached = $preview->isCached($fileId); + $this->assertEquals(\OC\Preview::THUMBNAILS_FOLDER . '/' . $fileId . '/' . $maxX . '-' . $maxY . '.png', $isCached); + + // Smaller previews should be based on the cached max preview + $smallX = 50; + $smallY = 50; + $preview = new \OC\Preview($this->user, 'files/', 'testimage.jpg', $smallX, $smallY); + $isCached = $preview->isCached($fileId); + + $this->assertEquals(\OC\Preview::THUMBNAILS_FOLDER . '/' . $fileId . '/' . $maxX . '-' . $maxY . '.png', $isCached); + + // A small preview should be created + $image = $preview->getPreview(); + $this->assertEquals($image->width(), $smallX); + $this->assertEquals($image->height(), $smallY); + + // The cache should contain the small preview + $thumbCacheFile = '/' . $this->user . '/' . \OC\Preview::THUMBNAILS_FOLDER . '/' . $fileId . '/' . $smallX . '-' . $smallY . '.png'; + + $this->assertEquals($this->rootView->file_exists($thumbCacheFile), true); + + // 2nd request should indicate that we have a cached copy of the exact dimension + $isCached = $preview->isCached($fileId); + + $this->assertEquals(\OC\Preview::THUMBNAILS_FOLDER . '/' . $fileId . '/' . $smallX . '-' . $smallY . '.png', $isCached); + } + public function testIsPreviewDeleted() { $sampleFile = '/'.$this->user.'/files/test.txt'; @@ -96,25 +169,6 @@ class Preview extends TestCase { $this->assertEquals($this->rootView->is_dir($thumbCacheFolder), false); } - public function testIsMaxSizeWorking() { - - $maxX = 250; - $maxY = 250; - - \OC_Config::setValue('preview_max_x', $maxX); - \OC_Config::setValue('preview_max_y', $maxY); - - $sampleFile = '/'.$this->user.'/files/test.txt'; - - $this->rootView->file_put_contents($sampleFile, 'dummy file data'); - - $preview = new \OC\Preview($this->user, 'files/', 'test.txt', 1000, 1000); - $image = $preview->getPreview(); - - $this->assertEquals($image->width(), $maxX); - $this->assertEquals($image->height(), $maxY); - } - public function txtBlacklist() { $txt = 'random text file'; diff --git a/tests/lib/share/share.php b/tests/lib/share/share.php index f35a0fa8e43..124ad450e2e 100644 --- a/tests/lib/share/share.php +++ b/tests/lib/share/share.php @@ -545,6 +545,13 @@ class Test_Share extends \Test\TestCase { // Valid share $this->shareUserOneTestFileWithGroupOne(); + // check if only the group share was created and not a single db-entry for each user + $statement = \OCP\DB::prepare('select `id` from `*PREFIX*share`'); + $query = $statement->execute(); + $result = $query->fetchAll(); + $this->assertSame(1, count($result)); + + // Attempt to share again OC_User::setUserId($this->user1); $message = 'Sharing test.txt failed, because this item is already shared with '.$this->group1; @@ -1128,6 +1135,240 @@ class Test_Share extends \Test\TestCase { \OC_Appconfig::deleteKey('core', 'shareapi_expire_after_n_days'); \OC_Appconfig::deleteKey('core', 'shareapi_enforce_expire_date'); } + + /** + * Cannot set password is there is no user + * + * @expectedException Exception + * @expectedExceptionMessage User not logged in + */ + public function testSetPasswordNoUser() { + $userSession = $this->getMockBuilder('\OCP\IUserSession') + ->disableOriginalConstructor() + ->getMock(); + + $connection = $this->getMockBuilder('\OC\DB\Connection') + ->disableOriginalConstructor() + ->getMock(); + + $config = $this->getMockBuilder('\OCP\IConfig') + ->disableOriginalConstructor() + ->getMock(); + + \OC\Share\Share::setPassword($userSession, $connection, $config, 1, 'pass'); + } + + /** + * Test setting a password when everything is fine + */ + public function testSetPassword() { + $user = $this->getMockBuilder('\OCP\IUser') + ->disableOriginalConstructor() + ->getMock(); + $user->method('getUID')->willReturn('user'); + + $userSession = $this->getMockBuilder('\OCP\IUserSession') + ->disableOriginalConstructor() + ->getMock(); + $userSession->method('getUser')->willReturn($user); + + + $ex = $this->getMockBuilder('\Doctrine\DBAL\Query\Expression\ExpressionBuilder') + ->disableOriginalConstructor() + ->getMock(); + $qb = $this->getMockBuilder('\Doctrine\DBAL\Query\QueryBuilder') + ->disableOriginalConstructor() + ->getMock(); + $qb->method('update')->will($this->returnSelf()); + $qb->method('set')->will($this->returnSelf()); + $qb->method('where')->will($this->returnSelf()); + $qb->method('andWhere')->will($this->returnSelf()); + $qb->method('select')->will($this->returnSelf()); + $qb->method('from')->will($this->returnSelf()); + $qb->method('setParameter')->will($this->returnSelf()); + $qb->method('expr')->willReturn($ex); + + $ret = $this->getMockBuilder('\Doctrine\DBAL\Driver\ResultStatement') + ->disableOriginalConstructor() + ->getMock(); + $ret->method('fetch')->willReturn(['uid_owner' => 'user']); + $qb->method('execute')->willReturn($ret); + + + $connection = $this->getMockBuilder('\OC\DB\Connection') + ->disableOriginalConstructor() + ->getMock(); + $connection->method('createQueryBuilder')->willReturn($qb); + + $config = $this->getMockBuilder('\OCP\IConfig') + ->disableOriginalConstructor() + ->getMock(); + + + $res = \OC\Share\Share::setPassword($userSession, $connection, $config, 1, 'pass'); + + $this->assertTrue($res); + } + + /** + * @expectedException Exception + * @expectedExceptionMessage Cannot remove password + * + * Test removing a password when password is enforced + */ + public function testSetPasswordRemove() { + $user = $this->getMockBuilder('\OCP\IUser') + ->disableOriginalConstructor() + ->getMock(); + $user->method('getUID')->willReturn('user'); + + $userSession = $this->getMockBuilder('\OCP\IUserSession') + ->disableOriginalConstructor() + ->getMock(); + $userSession->method('getUser')->willReturn($user); + + + $ex = $this->getMockBuilder('\Doctrine\DBAL\Query\Expression\ExpressionBuilder') + ->disableOriginalConstructor() + ->getMock(); + $qb = $this->getMockBuilder('\Doctrine\DBAL\Query\QueryBuilder') + ->disableOriginalConstructor() + ->getMock(); + $qb->method('update')->will($this->returnSelf()); + $qb->method('select')->will($this->returnSelf()); + $qb->method('from')->will($this->returnSelf()); + $qb->method('set')->will($this->returnSelf()); + $qb->method('where')->will($this->returnSelf()); + $qb->method('andWhere')->will($this->returnSelf()); + $qb->method('setParameter')->will($this->returnSelf()); + $qb->method('expr')->willReturn($ex); + + $ret = $this->getMockBuilder('\Doctrine\DBAL\Driver\ResultStatement') + ->disableOriginalConstructor() + ->getMock(); + $ret->method('fetch')->willReturn(['uid_owner' => 'user']); + $qb->method('execute')->willReturn($ret); + + + $connection = $this->getMockBuilder('\OC\DB\Connection') + ->disableOriginalConstructor() + ->getMock(); + $connection->method('createQueryBuilder')->willReturn($qb); + + $config = $this->getMockBuilder('\OCP\IConfig') + ->disableOriginalConstructor() + ->getMock(); + $config->method('getAppValue')->willReturn('yes'); + + \OC\Share\Share::setPassword($userSession, $connection, $config, 1, ''); + } + + /** + * @expectedException Exception + * @expectedExceptionMessage Share not found + * + * Test modification of invaid share + */ + public function testSetPasswordInvalidShare() { + $user = $this->getMockBuilder('\OCP\IUser') + ->disableOriginalConstructor() + ->getMock(); + $user->method('getUID')->willReturn('user'); + + $userSession = $this->getMockBuilder('\OCP\IUserSession') + ->disableOriginalConstructor() + ->getMock(); + $userSession->method('getUser')->willReturn($user); + + + $ex = $this->getMockBuilder('\Doctrine\DBAL\Query\Expression\ExpressionBuilder') + ->disableOriginalConstructor() + ->getMock(); + $qb = $this->getMockBuilder('\Doctrine\DBAL\Query\QueryBuilder') + ->disableOriginalConstructor() + ->getMock(); + $qb->method('update')->will($this->returnSelf()); + $qb->method('set')->will($this->returnSelf()); + $qb->method('where')->will($this->returnSelf()); + $qb->method('andWhere')->will($this->returnSelf()); + $qb->method('select')->will($this->returnSelf()); + $qb->method('from')->will($this->returnSelf()); + $qb->method('setParameter')->will($this->returnSelf()); + $qb->method('expr')->willReturn($ex); + + $ret = $this->getMockBuilder('\Doctrine\DBAL\Driver\ResultStatement') + ->disableOriginalConstructor() + ->getMock(); + $ret->method('fetch')->willReturn([]); + $qb->method('execute')->willReturn($ret); + + + $connection = $this->getMockBuilder('\OC\DB\Connection') + ->disableOriginalConstructor() + ->getMock(); + $connection->method('createQueryBuilder')->willReturn($qb); + + $config = $this->getMockBuilder('\OCP\IConfig') + ->disableOriginalConstructor() + ->getMock(); + + + \OC\Share\Share::setPassword($userSession, $connection, $config, 1, 'pass'); + } + + /** + * @expectedException Exception + * @expectedExceptionMessage Cannot update share of a different user + * + * Test modification of share of another user + */ + public function testSetPasswordShareOtherUser() { + $user = $this->getMockBuilder('\OCP\IUser') + ->disableOriginalConstructor() + ->getMock(); + $user->method('getUID')->willReturn('user'); + + $userSession = $this->getMockBuilder('\OCP\IUserSession') + ->disableOriginalConstructor() + ->getMock(); + $userSession->method('getUser')->willReturn($user); + + + $ex = $this->getMockBuilder('\Doctrine\DBAL\Query\Expression\ExpressionBuilder') + ->disableOriginalConstructor() + ->getMock(); + $qb = $this->getMockBuilder('\Doctrine\DBAL\Query\QueryBuilder') + ->disableOriginalConstructor() + ->getMock(); + $qb->method('update')->will($this->returnSelf()); + $qb->method('set')->will($this->returnSelf()); + $qb->method('where')->will($this->returnSelf()); + $qb->method('andWhere')->will($this->returnSelf()); + $qb->method('select')->will($this->returnSelf()); + $qb->method('from')->will($this->returnSelf()); + $qb->method('setParameter')->will($this->returnSelf()); + $qb->method('expr')->willReturn($ex); + + $ret = $this->getMockBuilder('\Doctrine\DBAL\Driver\ResultStatement') + ->disableOriginalConstructor() + ->getMock(); + $ret->method('fetch')->willReturn(['uid_owner' => 'user2']); + $qb->method('execute')->willReturn($ret); + + + $connection = $this->getMockBuilder('\OC\DB\Connection') + ->disableOriginalConstructor() + ->getMock(); + $connection->method('createQueryBuilder')->willReturn($qb); + + $config = $this->getMockBuilder('\OCP\IConfig') + ->disableOriginalConstructor() + ->getMock(); + + + \OC\Share\Share::setPassword($userSession, $connection, $config, 1, 'pass'); + } + } class DummyShareClass extends \OC\Share\Share { diff --git a/tests/lib/streamwrappers.php b/tests/lib/streamwrappers.php index fc3d02acae7..6216c5a4be8 100644 --- a/tests/lib/streamwrappers.php +++ b/tests/lib/streamwrappers.php @@ -72,6 +72,8 @@ class Test_StreamWrappers extends \Test\TestCase { } public function testOC() { + // FIXME: use proper tearDown with $this->loginAsUser() and $this->logout() + // (would currently break the tests for some reason) $originalStorage = \OC\Files\Filesystem::getStorage('/'); \OC\Files\Filesystem::clearMounts(); diff --git a/tests/lib/testcase.php b/tests/lib/testcase.php index a83be713194..e66dfb13353 100644 --- a/tests/lib/testcase.php +++ b/tests/lib/testcase.php @@ -167,9 +167,9 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase { * Login and setup FS as a given user, * sets the given user as the current user. * - * @param string $user user id + * @param string $user user id or empty for a generic FS */ - static protected function loginAsUser($user) { + static protected function loginAsUser($user = '') { self::logout(); \OC\Files\Filesystem::tearDown(); \OC_User::setUserId($user); diff --git a/tests/settings/controller/AppSettingsControllerTest.php b/tests/settings/controller/AppSettingsControllerTest.php new file mode 100644 index 00000000000..d6379deb9c8 --- /dev/null +++ b/tests/settings/controller/AppSettingsControllerTest.php @@ -0,0 +1,231 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Settings\Controller; + +use OCP\AppFramework\Http\ContentSecurityPolicy; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\TemplateResponse; +use Test\TestCase; +use OCP\IRequest; +use OCP\IL10N; +use OCP\IConfig; +use OCP\ICache; +use OCP\INavigationManager; +use OCP\App\IAppManager; +use OC\OCSClient; + +/** + * Class AppSettingsControllerTest + * + * @package OC\Settings\Controller + */ +class AppSettingsControllerTest extends TestCase { + /** @var AppSettingsController */ + private $appSettingsController; + /** @var IRequest */ + private $request; + /** @var IL10N */ + private $l10n; + /** @var IConfig */ + private $config; + /** @var ICache */ + private $cache; + /** @var INavigationManager */ + private $navigationManager; + /** @var IAppManager */ + private $appManager; + /** @var OCSClient */ + private $ocsClient; + + public function setUp() { + parent::setUp(); + + $this->request = $this->getMockBuilder('\OCP\IRequest') + ->disableOriginalConstructor()->getMock(); + $this->l10n = $this->getMockBuilder('\OCP\IL10N') + ->disableOriginalConstructor()->getMock(); + $this->l10n->expects($this->any()) + ->method('t') + ->will($this->returnArgument(0)); + $this->config = $this->getMockBuilder('\OCP\IConfig') + ->disableOriginalConstructor()->getMock(); + $cacheFactory = $this->getMockBuilder('\OCP\ICacheFactory') + ->disableOriginalConstructor()->getMock(); + $this->cache = $this->getMockBuilder('\OCP\ICache') + ->disableOriginalConstructor()->getMock(); + $cacheFactory + ->expects($this->once()) + ->method('create') + ->with('settings') + ->will($this->returnValue($this->cache)); + + $this->navigationManager = $this->getMockBuilder('\OCP\INavigationManager') + ->disableOriginalConstructor()->getMock(); + $this->appManager = $this->getMockBuilder('\OCP\App\IAppManager') + ->disableOriginalConstructor()->getMock(); + $this->ocsClient = $this->getMockBuilder('\OC\OCSClient') + ->disableOriginalConstructor()->getMock(); + + $this->appSettingsController = new AppSettingsController( + 'settings', + $this->request, + $this->l10n, + $this->config, + $cacheFactory, + $this->navigationManager, + $this->appManager, + $this->ocsClient + ); + } + + public function testChangeExperimentalConfigStateTrue() { + $this->config + ->expects($this->once()) + ->method('setSystemValue') + ->with('appstore.experimental.enabled', true); + $this->appManager + ->expects($this->once()) + ->method('clearAppsCache'); + $this->assertEquals(new DataResponse(), $this->appSettingsController->changeExperimentalConfigState(true)); + } + + public function testChangeExperimentalConfigStateFalse() { + $this->config + ->expects($this->once()) + ->method('setSystemValue') + ->with('appstore.experimental.enabled', false); + $this->appManager + ->expects($this->once()) + ->method('clearAppsCache'); + $this->assertEquals(new DataResponse(), $this->appSettingsController->changeExperimentalConfigState(false)); + } + + public function testListCategoriesCached() { + $this->cache + ->expects($this->exactly(2)) + ->method('get') + ->with('listCategories') + ->will($this->returnValue(['CachedArray'])); + $this->assertSame(['CachedArray'], $this->appSettingsController->listCategories()); + } + + public function testListCategoriesNotCachedWithoutAppStore() { + $expected = [ + [ + 'id' => 0, + 'displayName' => 'Enabled', + ], + [ + 'id' => 1, + 'displayName' => 'Not enabled', + ], + ]; + $this->cache + ->expects($this->once()) + ->method('get') + ->with('listCategories') + ->will($this->returnValue(null)); + $this->cache + ->expects($this->once()) + ->method('set') + ->with('listCategories', $expected, 3600); + + + $this->assertSame($expected, $this->appSettingsController->listCategories()); + } + + public function testListCategoriesNotCachedWithAppStore() { + $expected = [ + [ + 'id' => 0, + 'displayName' => 'Enabled', + ], + [ + 'id' => 1, + 'displayName' => 'Not enabled', + ], + [ + 'id' => 0, + 'displayName' => 'Tools', + ], + [ + 'id' => 1, + 'displayName' => 'Awesome Games', + ], + [ + 'id' => 2, + 'displayName' => 'PIM', + ], + [ + 'id' => 3, + 'displayName' => 'Papershop', + ], + ]; + + $this->cache + ->expects($this->once()) + ->method('get') + ->with('listCategories') + ->will($this->returnValue(null)); + $this->cache + ->expects($this->once()) + ->method('set') + ->with('listCategories', $expected, 3600); + + $this->ocsClient + ->expects($this->once()) + ->method('isAppStoreEnabled') + ->will($this->returnValue(true)); + $this->ocsClient + ->expects($this->once()) + ->method('getCategories') + ->will($this->returnValue( + [ + 'ownCloud Tools', + 'Awesome Games', + 'ownCloud PIM', + 'Papershop', + ] + )); + + $this->assertSame($expected, $this->appSettingsController->listCategories()); + } + + public function testViewApps() { + $this->config + ->expects($this->once()) + ->method('getSystemValue') + ->with('appstore.experimental.enabled', false); + $this->navigationManager + ->expects($this->once()) + ->method('setActiveEntry') + ->with('core_apps'); + + $policy = new ContentSecurityPolicy(); + $policy->addAllowedImageDomain('https://apps.owncloud.com'); + + $expected = new TemplateResponse('settings', 'apps', ['experimentalEnabled' => false], 'user'); + $expected->setContentSecurityPolicy($policy); + + $this->assertEquals($expected, $this->appSettingsController->viewApps()); + } +} |