aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.htaccess2
-rw-r--r--apps/files_sharing/css/public.scss9
-rw-r--r--apps/files_sharing/lib/Template/LinkMenuAction.php8
-rw-r--r--apps/files_sharing/tests/UpdaterTest.php2
-rw-r--r--apps/files_trashbin/tests/TrashbinTest.php2
-rw-r--r--apps/updatenotification/appinfo/routes.php16
-rw-r--r--apps/updatenotification/composer/composer/autoload_classmap.php1
-rw-r--r--apps/updatenotification/composer/composer/autoload_static.php1
-rw-r--r--apps/updatenotification/css/admin.css35
-rw-r--r--apps/updatenotification/js-src/components/root.vue101
-rw-r--r--apps/updatenotification/lib/Controller/APIController.php134
-rw-r--r--core/Controller/NavigationController.php33
-rw-r--r--core/css/header.scss242
-rw-r--r--core/css/mobile.scss33
-rw-r--r--core/css/public.scss34
-rw-r--r--core/js/contactsmenu.js2
-rw-r--r--core/js/js.js15
-rw-r--r--core/templates/layout.public.php22
-rw-r--r--core/templates/layout.user.php2
-rw-r--r--lib/base.php2
-rw-r--r--lib/private/App/AppManager.php7
-rw-r--r--lib/private/App/AppStore/Fetcher/AppFetcher.php4
-rw-r--r--lib/private/App/AppStore/Fetcher/Fetcher.php2
-rw-r--r--lib/private/Collaboration/Collaborators/MailPlugin.php13
-rw-r--r--lib/private/DB/Connection.php2
-rw-r--r--lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php14
-rw-r--r--lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php6
-rw-r--r--lib/private/DB/QueryBuilder/ExpressionBuilder/SqliteExpressionBuilder.php4
-rw-r--r--lib/private/DB/QueryBuilder/FunctionBuilder/FunctionBuilder.php4
-rw-r--r--lib/private/DB/QueryBuilder/QueryBuilder.php10
-rw-r--r--lib/private/Security/IdentityProof/Key.php7
-rw-r--r--lib/private/Security/IdentityProof/Manager.php11
-rw-r--r--lib/private/Security/IdentityProof/Signer.php5
-rw-r--r--lib/private/Updater.php2
-rw-r--r--lib/private/User/Database.php79
-rw-r--r--lib/private/legacy/app.php39
-rw-r--r--lib/public/DB/QueryBuilder/IFunctionBuilder.php9
-rw-r--r--settings/ajax/disableapp.php2
-rw-r--r--tests/Core/Controller/NavigationControllerTest.php31
-rw-r--r--tests/lib/DB/QueryBuilder/ExpressionBuilderTest.php4
-rw-r--r--tests/lib/DB/QueryBuilder/FunctionBuilderTest.php10
-rw-r--r--tests/lib/User/Backend.php12
-rw-r--r--tests/lib/User/DatabaseTest.php36
44 files changed, 717 insertions, 293 deletions
diff --git a/.gitignore b/.gitignore
index 82c2a272bad..ec3043c8a33 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,6 +34,7 @@
!/apps/updatenotification
/apps/updatenotification/js/merged.js
/apps/updatenotification/js/merged.js.map
+/apps/updatenotification/js/*.hot-update.*
/apps/updatenotification/node_modules
!/apps/theming
!/apps/twofactor_backupcodes
diff --git a/.htaccess b/.htaccess
index 81dae47a34d..190b414558b 100644
--- a/.htaccess
+++ b/.htaccess
@@ -23,7 +23,7 @@
<FilesMatch "\.(css|js|svg|gif)$">
Header set Cache-Control "max-age=15778463"
</FilesMatch>
-
+
# Let browsers cache WOFF files for a week
<FilesMatch "\.woff$">
Header set Cache-Control "max-age=604800"
diff --git a/apps/files_sharing/css/public.scss b/apps/files_sharing/css/public.scss
index 1e9dec87fad..e040d449129 100644
--- a/apps/files_sharing/css/public.scss
+++ b/apps/files_sharing/css/public.scss
@@ -88,15 +88,6 @@ thead {
opacity: .5;
}
-#directLink-container {
- flex-wrap: wrap;
-}
-
-#directLink {
- margin-left: 30px;
- flex-basis: 100%;
-}
-
/* header buttons */
#details {
display: inline-flex;
diff --git a/apps/files_sharing/lib/Template/LinkMenuAction.php b/apps/files_sharing/lib/Template/LinkMenuAction.php
index 2fdf83e7026..519bc553228 100644
--- a/apps/files_sharing/lib/Template/LinkMenuAction.php
+++ b/apps/files_sharing/lib/Template/LinkMenuAction.php
@@ -47,8 +47,12 @@ class LinkMenuAction extends SimpleMenuAction {
'<a id="directLink-container">' .
'<span class="icon ' . Util::sanitizeHTML($this->getIcon()) . '"></span>' .
'<label for="directLink">' . Util::sanitizeHTML($this->getLabel()) . '</label>' .
- '<input id="directLink" type="text" readonly="" value="' . Util::sanitizeHTML($this->getLink()) . '">' .
'</a>' .
+ '</li>' .
+ '<li>' .
+ '<span class="menuitem">' .
+ '<input id="directLink" type="text" readonly="" value="' . Util::sanitizeHTML($this->getLink()) . '">' .
+ '</span>' .
'</li>';
}
-} \ No newline at end of file
+}
diff --git a/apps/files_sharing/tests/UpdaterTest.php b/apps/files_sharing/tests/UpdaterTest.php
index c8d089f54b4..2e044716d07 100644
--- a/apps/files_sharing/tests/UpdaterTest.php
+++ b/apps/files_sharing/tests/UpdaterTest.php
@@ -127,7 +127,7 @@ class UpdaterTest extends TestCase {
$rootView->deleteAll('files_trashin');
if ($status === false) {
- \OC_App::disable('files_trashbin');
+ \OC::$server->getAppManager()->disableApp('files_trashbin');
}
\OC\Files\Filesystem::getLoader()->removeStorageWrapper('oc_trashbin');
diff --git a/apps/files_trashbin/tests/TrashbinTest.php b/apps/files_trashbin/tests/TrashbinTest.php
index 7e4cdb112e8..1121940c84e 100644
--- a/apps/files_trashbin/tests/TrashbinTest.php
+++ b/apps/files_trashbin/tests/TrashbinTest.php
@@ -71,7 +71,7 @@ class TrashbinTest extends \Test\TestCase {
$application->registerMountProviders();
//disable encryption
- \OC_App::disable('encryption');
+ \OC::$server->getAppManager()->disableApp('encryption');
$config = \OC::$server->getConfig();
//configure trashbin
diff --git a/apps/updatenotification/appinfo/routes.php b/apps/updatenotification/appinfo/routes.php
index fd508d37051..86191680c41 100644
--- a/apps/updatenotification/appinfo/routes.php
+++ b/apps/updatenotification/appinfo/routes.php
@@ -21,10 +21,12 @@
*
*/
-use OCA\UpdateNotification\AppInfo\Application;
-
-$application = new Application();
-$application->registerRoutes($this, ['routes' => [
- ['name' => 'Admin#createCredentials', 'url' => '/credentials', 'verb' => 'GET'],
- ['name' => 'Admin#setChannel', 'url' => '/channel', 'verb' => 'POST'],
-]]);
+return [
+ 'routes' => [
+ ['name' => 'Admin#createCredentials', 'url' => '/credentials', 'verb' => 'GET'],
+ ['name' => 'Admin#setChannel', 'url' => '/channel', 'verb' => 'POST'],
+ ],
+ 'ocs' => [
+ ['name' => 'API#getAppList', 'url' => '/api/{apiVersion}/applist/{newVersion}', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v1']],
+ ],
+];
diff --git a/apps/updatenotification/composer/composer/autoload_classmap.php b/apps/updatenotification/composer/composer/autoload_classmap.php
index 55ad497a858..bc9474d5458 100644
--- a/apps/updatenotification/composer/composer/autoload_classmap.php
+++ b/apps/updatenotification/composer/composer/autoload_classmap.php
@@ -7,6 +7,7 @@ $baseDir = $vendorDir;
return array(
'OCA\\UpdateNotification\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
+ 'OCA\\UpdateNotification\\Controller\\APIController' => $baseDir . '/../lib/Controller/APIController.php',
'OCA\\UpdateNotification\\Controller\\AdminController' => $baseDir . '/../lib/Controller/AdminController.php',
'OCA\\UpdateNotification\\Notification\\BackgroundJob' => $baseDir . '/../lib/Notification/BackgroundJob.php',
'OCA\\UpdateNotification\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php',
diff --git a/apps/updatenotification/composer/composer/autoload_static.php b/apps/updatenotification/composer/composer/autoload_static.php
index 4a50e180a8f..1908e433941 100644
--- a/apps/updatenotification/composer/composer/autoload_static.php
+++ b/apps/updatenotification/composer/composer/autoload_static.php
@@ -22,6 +22,7 @@ class ComposerStaticInitUpdateNotification
public static $classMap = array (
'OCA\\UpdateNotification\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
+ 'OCA\\UpdateNotification\\Controller\\APIController' => __DIR__ . '/..' . '/../lib/Controller/APIController.php',
'OCA\\UpdateNotification\\Controller\\AdminController' => __DIR__ . '/..' . '/../lib/Controller/AdminController.php',
'OCA\\UpdateNotification\\Notification\\BackgroundJob' => __DIR__ . '/..' . '/../lib/Notification/BackgroundJob.php',
'OCA\\UpdateNotification\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php',
diff --git a/apps/updatenotification/css/admin.css b/apps/updatenotification/css/admin.css
index c136b61d097..cb03f6a15cb 100644
--- a/apps/updatenotification/css/admin.css
+++ b/apps/updatenotification/css/admin.css
@@ -1,4 +1,33 @@
-#updatenotification p,
-#oca_updatenotification_section p {
- margin: 25px 0;
+#updatenotification {
+ margin-top: -25px;
+}
+
+#updatenotification div.update,
+#updatenotification ul,
+#updatenotification p:not(.inlineblock) {
+ margin-bottom: 25px;
+}
+
+
+#updatenotification h2.inlineblock {
+ margin-top: 25px;
+}
+
+#updatenotification h3,
+#updatenotification h3 .icon {
+ cursor: pointer;
+}
+
+#updatenotification h3:first-of-type {
+ margin-top: 0;
+}
+
+#updatenotification .icon {
+ display: inline-block;
+ margin-bottom: -3px;
+}
+
+#updatenotification .icon-triangle-s,
+#updatenotification .icon-triangle-n {
+ opacity: 0.5;
}
diff --git a/apps/updatenotification/js-src/components/root.vue b/apps/updatenotification/js-src/components/root.vue
index a6c78d9cdf0..11dfd351c63 100644
--- a/apps/updatenotification/js-src/components/root.vue
+++ b/apps/updatenotification/js-src/components/root.vue
@@ -1,9 +1,36 @@
<template>
<div id="updatenotification" class="followupsection">
- <p>
+ <div class="update">
<template v-if="isNewVersionAvailable">
- <strong>{{newVersionAvailableString}}</strong>
- <input v-if="updaterEnabled" type="button" @click="clickUpdaterButton" id="oca_updatenotification_button" :value="t('updatenotification', 'Open updater')">
+ <p>
+ <span v-html="newVersionAvailableString"></span><br>
+ <span v-if="!isListFetched" class="icon icon-loading-small"></span>
+ <span v-html="statusText"></span>
+ </p>
+
+ <template v-if="missingAppUpdates.length">
+ <h3 @click="toggleHideMissingUpdates">
+ {{ t('updatenotification', 'Apps missing updates') }}
+ <span v-if="!hideMissingUpdates" class="icon icon-triangle-n"></span>
+ <span v-if="hideMissingUpdates" class="icon icon-triangle-s"></span>
+ </h3>
+ <ul class="applist" v-if="!hideMissingUpdates">
+ <li v-for="app in missingAppUpdates"><a :href="'https://apps.nextcloud.com/apps/' + app.appId" :title="t('settings', 'View in store')">{{app.appName}} ↗</a></li>
+ </ul>
+ </template>
+
+ <template v-if="availableAppUpdates.length">
+ <h3 @click="toggleHideAvailableUpdates">
+ {{ t('updatenotification', 'Apps with available updates') }}
+ <span v-if="!hideAvailableUpdates" class="icon icon-triangle-n"></span>
+ <span v-if="hideAvailableUpdates" class="icon icon-triangle-s"></span>
+ </h3>
+ <ul class="applist">
+ <li v-for="app in availableAppUpdates" v-if="!hideAvailableUpdates"><a :href="'https://apps.nextcloud.com/apps/' + app.appId" :title="t('settings', 'View in store')">{{app.appName}} ↗</a></li>
+ </ul>
+ </template>
+
+ <a v-if="updaterEnabled" href="#" class="button" @click="clickUpdaterButton">{{ t('updatenotification', 'Open updater') }}</a>
<a v-if="downloadLink" :href="downloadLink" class="button" :class="{ hidden: !updaterEnabled }">{{ t('updatenotification', 'Download now') }}</a>
</template>
<template v-else-if="!isUpdateChecked">{{ t('updatenotification', 'The update check is not yet finished. Please refresh the page.') }}</template>
@@ -16,7 +43,7 @@
<br />
<em>{{ t('updatenotification', 'A non-default update server is in use to be checked for updates:') }} <code>{{updateServerURL}}</code></em>
</template>
- </p>
+ </div>
<p>
<label for="release-channel">{{ t('updatenotification', 'Update channel:') }}</label>
@@ -58,7 +85,15 @@
notifyGroups: '',
availableGroups: [],
isDefaultUpdateServerURL: true,
- enableChangeWatcher: false
+ enableChangeWatcher: false,
+
+ availableAppUpdates: [],
+ missingAppUpdates: [],
+ appStoreFailed: false,
+ appStoreDisabled: false,
+ isListFetched: false,
+ hideMissingUpdates: false,
+ hideAvailableUpdates: true
};
},
@@ -78,19 +113,67 @@
});
OCP.AppConfig.setValue('updatenotification', 'notify_groups', JSON.stringify(selectedGroups));
+ },
+ isNewVersionAvailable: function() {
+ if (!this.isNewVersionAvailable) {
+ return;
+ }
+
+ $.ajax({
+ url: OC.linkToOCS('apps/updatenotification/api/v1/applist', 2) + this.newVersionString,
+ type: 'GET',
+ beforeSend: function (request) {
+ request.setRequestHeader('Accept', 'application/json');
+ },
+ success: function(response) {
+ this.availableAppUpdates = response.ocs.data.available;
+ this.missingAppUpdates = response.ocs.data.missing;
+ this.isListFetched = true;
+ this.appStoreFailed = false;
+ }.bind(this),
+ error: function(xhr) {
+ this.availableAppUpdates = [];
+ this.missingAppUpdates = [];
+ this.appStoreDisabled = xhr.responseJSON.ocs.data.appstore_disabled;
+ this.isListFetched = true;
+ this.appStoreFailed = true;
+ }.bind(this)
+ });
}
},
computed: {
newVersionAvailableString: function() {
- return t('updatenotification', 'A new version is available: {newVersionString}', {
+ return t('updatenotification', 'A new version is available: <strong>{newVersionString}</strong>', {
newVersionString: this.newVersionString
});
},
+
lastCheckedOnString: function() {
return t('updatenotification', 'Checked on {lastCheckedDate}', {
lastCheckedDate: this.lastCheckedDate
});
+ },
+
+ statusText: function() {
+ if (!this.isListFetched) {
+ return t('updatenotification', 'Checking apps for compatible updates');
+ }
+
+ if (this.appstoreDisabled) {
+ return t('updatenotification', 'Please make sure your config.php does not set <samp>appstoreenabled</samp> to false.');
+ }
+
+ if (this.appstoreFailed) {
+ return t('updatenotification', 'Could not connect to the appstore or the appstore returned no updates at all. Search manually for updates or make sure your server has access to the internet and can connect to the appstore.');
+ }
+
+ return this.missingAppUpdates.length === 0 ? t('updatenotification', '<strong>All</strong> apps have an update for this version available', this) : n('updatenotification',
+ '<strong>%n</strong> app has no update for this version available',
+ '<strong>%n</strong> apps have no update for this version available',
+ this.missingAppUpdates.length, {
+ version: this.newVersionString
+ });
}
},
@@ -144,6 +227,12 @@
OC.msg.finishedAction('#channel_save_msg', data);
}
});
+ },
+ toggleHideMissingUpdates: function() {
+ this.hideMissingUpdates = !this.hideMissingUpdates;
+ },
+ toggleHideAvailableUpdates: function() {
+ this.hideAvailableUpdates = !this.hideAvailableUpdates;
}
},
diff --git a/apps/updatenotification/lib/Controller/APIController.php b/apps/updatenotification/lib/Controller/APIController.php
new file mode 100644
index 00000000000..90a5fb782f5
--- /dev/null
+++ b/apps/updatenotification/lib/Controller/APIController.php
@@ -0,0 +1,134 @@
+<?php
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\UpdateNotification\Controller;
+
+use OC\App\AppStore\Fetcher\AppFetcher;
+use OCP\App\AppPathNotFoundException;
+use OCP\App\IAppManager;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\OCSController;
+use OCP\IConfig;
+use OCP\IRequest;
+
+class APIController extends OCSController {
+
+ /** @var IConfig */
+ protected $config;
+
+ /** @var IAppManager */
+ protected $appManager;
+
+ /** @var AppFetcher */
+ protected $appFetcher;
+
+ /**
+ * @param string $appName
+ * @param IRequest $request
+ * @param IConfig $config
+ * @param IAppManager $appManager
+ * @param AppFetcher $appFetcher
+ */
+ public function __construct($appName,
+ IRequest $request,
+ IConfig $config,
+ IAppManager $appManager,
+ AppFetcher $appFetcher) {
+ parent::__construct($appName, $request);
+
+ $this->config = $config;
+ $this->appManager = $appManager;
+ $this->appFetcher = $appFetcher;
+ }
+
+ /**
+ * @param string $newVersion
+ * @return DataResponse
+ */
+ public function getAppList(string $newVersion): DataResponse {
+ if (!$this->config->getSystemValue('appstoreenabled', true)) {
+ return new DataResponse([
+ 'appstore_disabled' => true,
+ ], Http::STATUS_NOT_FOUND);
+ }
+
+ // Get list of installed custom apps
+ $installedApps = $this->appManager->getInstalledApps();
+ $installedApps = array_filter($installedApps, function($app) {
+ try {
+ $this->appManager->getAppPath($app);
+ } catch (AppPathNotFoundException $e) {
+ return false;
+ }
+ return !$this->appManager->isShipped($app);
+ });
+
+ if (empty($installedApps)) {
+ return new DataResponse([
+ 'missing' => [],
+ 'available' => [],
+ ]);
+ }
+
+ $this->appFetcher->setVersion($newVersion, 'future-apps.json');
+
+ // Apps available on the app store for that version
+ $availableApps = array_map(function(array $app) {
+ return $app['id'];
+ }, $this->appFetcher->get());
+
+ if (empty($availableApps)) {
+ return new DataResponse([
+ 'appstore_disabled' => false,
+ 'already_on_latest' => false,
+ ], Http::STATUS_NOT_FOUND);
+ }
+
+ $missing = array_diff($installedApps, $availableApps);
+ $missing = array_map([$this, 'getAppDetails'], $missing);
+ sort($missing);
+
+ $available = array_intersect($installedApps, $availableApps);
+ $available = array_map([$this, 'getAppDetails'], $available);
+ sort($available);
+
+ return new DataResponse([
+ 'missing' => $missing,
+ 'available' => $available,
+ ]);
+ }
+
+ /**
+ * Get translated app name
+ *
+ * @param string $appId
+ * @return string[]
+ */
+ protected function getAppDetails($appId): array {
+ $app = $this->appManager->getAppInfo($appId);
+ return [
+ 'appId' => $appId,
+ 'appName' => $app['name'] ?? $appId,
+ ];
+ }
+}
diff --git a/core/Controller/NavigationController.php b/core/Controller/NavigationController.php
index 3521fac3b46..2397fb3c7b4 100644
--- a/core/Controller/NavigationController.php
+++ b/core/Controller/NavigationController.php
@@ -22,6 +22,7 @@
*/
namespace OC\Core\Controller;
+use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\INavigationManager;
@@ -54,7 +55,14 @@ class NavigationController extends OCSController {
if ($absolute) {
$navigation = $this->rewriteToAbsoluteUrls($navigation);
}
- return new DataResponse($navigation);
+
+ $etag = $this->generateETag($navigation);
+ if ($this->request->getHeader('If-None-Match') === $etag) {
+ return new DataResponse([], Http::STATUS_NOT_MODIFIED);
+ }
+ $response = new DataResponse($navigation);
+ $response->setETag($etag);
+ return $response;
}
/**
@@ -69,7 +77,28 @@ class NavigationController extends OCSController {
if ($absolute) {
$navigation = $this->rewriteToAbsoluteUrls($navigation);
}
- return new DataResponse($navigation);
+ $etag = $this->generateETag($navigation);
+ if ($this->request->getHeader('If-None-Match') === $etag) {
+ return new DataResponse([], Http::STATUS_NOT_MODIFIED);
+ }
+ $response = new DataResponse($navigation);
+ $response->setETag($etag);
+ return $response;
+ }
+
+ /**
+ * Generate an ETag for a list of navigation entries
+ *
+ * @param array $navigation
+ * @return string
+ */
+ private function generateETag(array $navigation): string {
+ foreach ($navigation as &$nav) {
+ if ($nav['id'] === 'logout') {
+ $nav['href'] = 'logout';
+ }
+ }
+ return md5(json_encode($navigation));
}
/**
diff --git a/core/css/header.scss b/core/css/header.scss
index 7021762bf7f..86739240aeb 100644
--- a/core/css/header.scss
+++ b/core/css/header.scss
@@ -74,13 +74,17 @@
#header {
/* Header menu */
.menu {
- top: 45px;
background-color: $color-main-background;
filter: drop-shadow(0 1px 10px $color-box-shadow);
border-radius: 0 0 3px 3px;
box-sizing: border-box;
z-index: 2000;
position: absolute;
+ max-width: 350px;
+ max-height: 280px;
+ right: 0;
+ top: 44px;
+ margin: 0;
&:not(.popovermenu) {
display: none;
@@ -96,6 +100,7 @@
width: 0;
position: absolute;
pointer-events: none;
+ right: 12px;
}
}
.logo {
@@ -151,6 +156,25 @@
#header-right, .header-right {
justify-content: flex-end;
}
+
+ /* Right header standard */
+ .header-right {
+ > div,
+ > form {
+ position: relative;
+ > .menutoggle {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 44px;
+ height: 44px;
+ cursor: pointer;
+ opacity: 0.6;
+ padding: 0;
+ margin: 0;
+ }
+ }
+ }
}
/* hover effect for app switcher label */
@@ -199,16 +223,17 @@
}
/* NAVIGATION --------------------------------------------------------------- */
-nav {
+nav[role='navigation'] {
display: inline-block;
width: 44px;
height: 44px;
- margin-left: -54px;
+ margin-left: -44px;
}
.header-left #navigation {
position: relative;
- left: -100%;
+ left: 22px; /* half the togglemenu */
+ transform: translateX(-50%);
width: 160px;
}
@@ -219,7 +244,7 @@ nav {
filter: drop-shadow(0 1px 10px $color-box-shadow);
&:after {
/* position of dropdown arrow */
- left: 47%;
+ left: 50%;
bottom: 100%;
border: solid transparent;
content: ' ';
@@ -229,26 +254,11 @@ nav {
pointer-events: none;
border-color: rgba(0, 0, 0, 0);
border-bottom-color: $color-main-background;
- border-width: 9px;
- margin-left: -9px;
+ border-width: 10px;
+ margin-left: -10px; /* border width */
}
}
-/* arrow look */
-#expanddiv:after {
- bottom: 100%;
- border: solid transparent;
- content: ' ';
- height: 0;
- width: 0;
- position: absolute;
- pointer-events: none;
- border-color: transparent;
- border-bottom-color: $color-main-background;
- border-width: 10px;
- margin-left: -10px;
-}
-
#navigation {
box-sizing: border-box;
* {
@@ -259,10 +269,10 @@ nav {
}
a {
position: relative;
- display: block;
+ display: inline-flex;
padding: 10px 12px;
- height:40px;
- vertical-align: text-bottom;
+ height: 40px;
+ align-items: center;
span {
display: inline-block;
padding-bottom: 0;
@@ -277,9 +287,6 @@ nav {
span {
opacity: .7;
}
- svg {
- margin-bottom: 2px;
- }
&:hover svg,
&:focus svg,
&:hover span,
@@ -299,20 +306,20 @@ nav {
max-width: 32px;
}
-}
-
-/* loading feedback for apps */
-.app-loading {
- .icon-loading-small-dark {
- display: inline !important;
- position: absolute;
- left: 12px;
- width: 16px;
- height: 16px;
- }
- .app-icon {
- opacity: 0;
+ /* loading feedback for apps */
+ .app-loading {
+ .icon-loading-small {
+ display: inline !important;
+ position: absolute;
+ left: 12px;
+ width: 16px;
+ height: 16px;
+ }
+ .app-icon {
+ opacity: 0;
+ }
}
+
}
/* Apps management */
@@ -335,99 +342,89 @@ nav {
display: inline-block;
color: rgba($color-primary-text, 0.7);
cursor: pointer;
- .icon-loading-small-dark {
- display: inline-block;
- margin-bottom: -3px;
- margin-right: 6px;
- background-size: 16px 16px;
- }
+ margin-right: 13px;
flex: 0 0 auto;
-}
-/* User menu on the right */
-#expand {
- position: relative;
- display: flex;
- align-items: center;
- padding: 7px 20px 6px 10px;
- cursor: pointer;
- * {
- cursor: pointer;
- }
- img {
- opacity: .7;
- margin-bottom: -2px;
- }
- &:hover,
- &:focus,
- &:active {
- color: $color-primary-text;
- img,
- #expandDisplayName {
- opacity: 1;
- }
- }
+ /* User menu on the right */
+ #expand {
+ opacity: 1; /* override icon opacity */
- /* Profile picture in header */
- .avatardiv {
- cursor: pointer;
- height: 32px;
- width: 32px;
img {
- opacity: 1;
- cursor: pointer;
+ opacity: .7;
+ margin-bottom: -2px;
}
- /* do not show display name when profile picture is present */
- &.avatardiv-shown + #expandDisplayName {
- display: none;
+ &:hover,
+ &:focus,
+ &:active {
+ color: $color-primary-text;
+
+ img, #expandDisplayName {
+ opacity: 1;
+ }
}
- }
- #expandDisplayName {
- padding: 8px;
- opacity: .6;
- }
-}
+ /* Profile picture in header */
+ .avatardiv {
+ cursor: pointer;
+ height: 32px;
+ width: 32px;
-/* full opacity for gear icon if active */
-#body-settings #expandDisplayName {
- opacity: 1;
-}
+ img {
+ opacity: 1;
+ cursor: pointer;
+ }
+ /* do not show display name when profile picture is present */
+ &.avatardiv-shown + #expandDisplayName {
+ display: none;
+ }
+ }
-/* show triangle below user menu if active */
-#body-settings #expand:before {
- content: ' ';
- height: 0;
- width: 0;
- position: absolute;
- pointer-events: none;
- border: 0 solid transparent;
- border-bottom-color: $color-main-background;
- border-width: 10px;
- transform: translateX(-50%);
- left: 26px;
- bottom: 0;
- z-index: 100;
- display: block;
+ #expandDisplayName {
+ padding: 8px;
+ opacity: .6;
+
+ /* full opacity for gear icon if active */
+ #body-settings & {
+ opacity: 1;
+ }
+ }
+
+ /* show triangle below user menu if active */
+ #body-settings &:before {
+ content: ' ';
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+ border: 0 solid transparent;
+ border-bottom-color: $color-main-background;
+ border-width: 10px;
+ bottom: 0;
+ z-index: 100;
+ display: block;
+ }
+ }
}
#expanddiv {
- right: 13px;
- background: $color-main-background;
- &:after {
- /* position of dropdown arrow */
- right: 13px;
- }
a {
- display: block;
+ display: inline-flex;
+ align-items: center;
height: 40px;
color: $color-main-text;
- padding: 10px 12px 0;
+ padding: 12px;
box-sizing: border-box;
opacity: .7;
+ white-space: nowrap;
+
+ .icon-loading-small {
+ margin-right: 9px;
+ background-size: 16px 16px;
+ }
img {
- margin-bottom: -3px;
- margin-right: 6px;
+ margin-right: 9px;
+ height: 16px;
+ width: 16px;
}
&:hover,
&:focus,
@@ -463,10 +460,15 @@ nav {
opacity: .6;
}
}
- .app-loading .icon-loading-small-dark {
- top: 12px;
- width: 20px;
- height: 20px;
+ .app-loading {
+ > svg {
+ display: none;
+ }
+ .icon-loading-small-dark {
+ width: 20px;
+ height: 20px;
+ display: block !important;
+ }
}
diff --git a/core/css/mobile.scss b/core/css/mobile.scss
index 6f1583cb77a..ebc7e094cdb 100644
--- a/core/css/mobile.scss
+++ b/core/css/mobile.scss
@@ -131,3 +131,36 @@ table.multiselect thead {
/* end of media query */
}
+
+@media only screen and (max-width: 480px) {
+ #header .header-right .menu {
+ max-width: calc(100vw - 26px);
+ position: fixed;
+ right: 13px;
+ top: 45px;
+ &::after {
+ display: none !important;
+ }
+ }
+ /* Arrow directly child of menutoggle */
+ #header .header-right > div {
+ &.openedMenu{
+ &::after {
+ display: block;
+ }
+ }
+ &::after {
+ border: 10px solid transparent;
+ border-bottom-color: $color-main-background;
+ bottom: 0;
+ content: ' ';
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+ right: 13px;
+ z-index: 2001;
+ display: none;
+ }
+ }
+}
diff --git a/core/css/public.scss b/core/css/public.scss
index 3651e701c34..6a175de6431 100644
--- a/core/css/public.scss
+++ b/core/css/public.scss
@@ -1,22 +1,22 @@
#body-public {
- .header-right {
+ .header-right {
- span:not(.popovermenu) a {
- color: $color-primary-text;
- }
+ span:not(.popovermenu) a {
+ color: $color-primary-text;
+ }
- .menutoggle,
- #header-primary-action[class^='icon-'] {
- padding: 14px;
- padding-right: 40px;
- background-position: right 15px center;
- color: $color-primary-text;
- cursor: pointer;
- }
+ .menutoggle,
+ #header-primary-action[class^='icon-'] {
+ padding: 14px;
+ padding-right: 40px;
+ background-position: right 15px center;
+ color: $color-primary-text;
+ cursor: pointer;
+ }
- .menutoggle {
- padding-right: 10px;
- }
+ #header-secondary-action {
+ margin-right: 13px;
+ }
- }
-} \ No newline at end of file
+ }
+}
diff --git a/core/js/contactsmenu.js b/core/js/contactsmenu.js
index 9e7ec552830..b0f302e1599 100644
--- a/core/js/contactsmenu.js
+++ b/core/js/contactsmenu.js
@@ -464,7 +464,7 @@
OC.registerMenu(this._$trigger, this.$el, function() {
this._toggleVisibility(true);
- }.bind(this));
+ }.bind(this), true);
this.$el.on('beforeHide', function() {
this._toggleVisibility(false);
}.bind(this));
diff --git a/core/js/js.js b/core/js/js.js
index fa92508ff7a..3c6ababf764 100644
--- a/core/js/js.js
+++ b/core/js/js.js
@@ -678,9 +678,10 @@ var OCP = {},
* @param {jQuery} $toggle
* @param {jQuery} $menuEl
* @param {function|undefined} toggle callback invoked everytime the menu is opened
+ * @param {boolean} headerMenu is this a top right header menu?
* @returns {undefined}
*/
- registerMenu: function($toggle, $menuEl, toggle) {
+ registerMenu: function($toggle, $menuEl, toggle, headerMenu) {
var self = this;
$menuEl.addClass('menu');
$toggle.on('click.menu', function(event) {
@@ -696,6 +697,11 @@ var OCP = {},
// close it
self.hideMenus();
}
+
+ if (headerMenu === true) {
+ $menuEl.parent().addClass('openedMenu');
+ }
+
$menuEl.slideToggle(OC.menuSpeed, toggle);
OC._currentMenu = $menuEl;
OC._currentMenuToggle = $toggle;
@@ -730,6 +736,7 @@ var OCP = {},
}
});
}
+ $('.openedMenu').removeClass('openedMenu');
OC._currentMenu = null;
OC._currentMenuToggle = null;
},
@@ -1396,7 +1403,7 @@ function initCore() {
initSessionHeartBeat();
}
- OC.registerMenu($('#expand'), $('#expanddiv'));
+ OC.registerMenu($('#expand'), $('#expanddiv'), false, true);
// toggle for menus
$(document).on('mouseup.closemenus', function(event) {
@@ -1480,7 +1487,7 @@ function initCore() {
if(event.which === 1 && !event.ctrlKey && !event.metaKey) {
$page.find('img').remove();
$page.find('div').remove(); // prevent odd double-clicks
- $page.prepend($('<div/>').addClass('icon-loading-small-dark'));
+ $page.prepend($('<div/>').addClass('icon-loading-small'));
} else {
// Close navigation when opening menu entry in
// a new tab
@@ -1702,7 +1709,7 @@ OC.PasswordConfirmation = {
requiresPasswordConfirmation: function() {
var serverTimeDiff = this.pageLoadTime - (nc_pageLoad * 1000);
var timeSinceLogin = moment.now() - (serverTimeDiff + (nc_lastLogin * 1000));
-
+
// if timeSinceLogin > 30 minutes and user backend allows password confirmation
return (backendAllowsPasswordConfirmation && timeSinceLogin > 30 * 60 * 1000);
},
diff --git a/core/templates/layout.public.php b/core/templates/layout.public.php
index b9451fe05a2..d3c12f8fd96 100644
--- a/core/templates/layout.public.php
+++ b/core/templates/layout.public.php
@@ -54,16 +54,18 @@
</a>
</span>
<?php if($template->getActionCount()>1) { ?>
- <span id="header-actions-toggle" class="menutoggle icon-more-white"></span>
- <div id="share-menu" class="popovermenu menu">
- <ul>
- <?php
- /** @var \OCP\AppFramework\Http\Template\IMenuAction $action */
- foreach($template->getOtherActions() as $action) {
- print_unescaped($action->render());
- }
- ?>
- </ul>
+ <div id="header-secondary-action">
+ <span id="header-actions-toggle" class="menutoggle icon-more-white"></span>
+ <div id="share-menu" class="popovermenu menu">
+ <ul>
+ <?php
+ /** @var \OCP\AppFramework\Http\Template\IMenuAction $action */
+ foreach($template->getOtherActions() as $action) {
+ print_unescaped($action->render());
+ }
+ ?>
+ </ul>
+ </div>
</div>
<?php } ?>
</div>
diff --git a/core/templates/layout.user.php b/core/templates/layout.user.php
index c8c8ec84efa..e11620a3111 100644
--- a/core/templates/layout.user.php
+++ b/core/templates/layout.user.php
@@ -80,7 +80,7 @@
<defs><filter id="invertMenuMore-<?php p($entry['id']); ?>"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0"></feColorMatrix></filter></defs>
<image x="0" y="0" width="16" height="16" preserveAspectRatio="xMinYMin meet" filter="url(#invertMenuMore-<?php p($entry['id']); ?>)" xlink:href="<?php print_unescaped($entry['icon'] . '?v=' . $_['versionHash']); ?>" class="app-icon"></image>
</svg>
- <div class="icon-loading-small-dark" style="display:none;"></div>
+ <div class="icon-loading-small" style="display:none;"></div>
<span><?php p($entry['name']); ?></span>
</a>
</li>
diff --git a/lib/base.php b/lib/base.php
index c5eabe1910a..f0e139d92c8 100644
--- a/lib/base.php
+++ b/lib/base.php
@@ -949,7 +949,7 @@ class OC {
$appIds = (array)$request->getParam('appid');
foreach($appIds as $appId) {
$appId = \OC_App::cleanAppId($appId);
- \OC_App::disable($appId);
+ \OC::$server->getAppManager()->disableApp($appId);
}
\OC_JSON::success();
exit();
diff --git a/lib/private/App/AppManager.php b/lib/private/App/AppManager.php
index 4bf15c23fab..81f1369cb5e 100644
--- a/lib/private/App/AppManager.php
+++ b/lib/private/App/AppManager.php
@@ -287,6 +287,13 @@ class AppManager implements IAppManager {
}
unset($this->installedAppsCache[$appId]);
$this->appConfig->setValue($appId, 'enabled', 'no');
+
+ // run uninstall steps
+ $appData = $this->getAppInfo($appId);
+ if (!is_null($appData)) {
+ \OC_App::executeRepairSteps($appId, $appData['repair-steps']['uninstall']);
+ }
+
$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent(
ManagerEvent::EVENT_APP_DISABLE, $appId
));
diff --git a/lib/private/App/AppStore/Fetcher/AppFetcher.php b/lib/private/App/AppStore/Fetcher/AppFetcher.php
index 63121c079bd..a8fd9349343 100644
--- a/lib/private/App/AppStore/Fetcher/AppFetcher.php
+++ b/lib/private/App/AppStore/Fetcher/AppFetcher.php
@@ -133,9 +133,11 @@ class AppFetcher extends Fetcher {
/**
* @param string $version
+ * @param string $filename
*/
- public function setVersion($version) {
+ public function setVersion(string $version, string $fileName = 'apps.json') {
parent::setVersion($version);
+ $this->fileName = $fileName;
$this->setEndpoint();
}
}
diff --git a/lib/private/App/AppStore/Fetcher/Fetcher.php b/lib/private/App/AppStore/Fetcher/Fetcher.php
index 4dbc360c0c4..172584c522b 100644
--- a/lib/private/App/AppStore/Fetcher/Fetcher.php
+++ b/lib/private/App/AppStore/Fetcher/Fetcher.php
@@ -194,7 +194,7 @@ abstract class Fetcher {
* Set the current Nextcloud version
* @param string $version
*/
- public function setVersion($version) {
+ public function setVersion(string $version) {
$this->version = $version;
}
}
diff --git a/lib/private/Collaboration/Collaborators/MailPlugin.php b/lib/private/Collaboration/Collaborators/MailPlugin.php
index b4964825182..c3816161ce3 100644
--- a/lib/private/Collaboration/Collaborators/MailPlugin.php
+++ b/lib/private/Collaboration/Collaborators/MailPlugin.php
@@ -73,7 +73,7 @@ class MailPlugin implements ISearchPlugin {
* @since 13.0.0
*/
public function search($search, $limit, $offset, ISearchResult $searchResult) {
- $result = ['wide' => [], 'exact' => []];
+ $result = $userResults = ['wide' => [], 'exact' => []];
$userType = new SearchResultType('users');
$emailType = new SearchResultType('emails');
@@ -136,14 +136,13 @@ class MailPlugin implements ISearchPlugin {
}
if (!$this->isCurrentUser($cloud) && !$searchResult->hasResult($userType, $cloud->getUser())) {
- $singleResult = [[
+ $userResults['wide'][] = [
'label' => $contact['FN'] . " ($emailAddress)",
'value' => [
'shareType' => Share::SHARE_TYPE_USER,
'shareWith' => $cloud->getUser(),
- ]],
+ ],
];
- $searchResult->addResultSet($userType, $singleResult, []);
}
}
continue;
@@ -175,10 +174,13 @@ class MailPlugin implements ISearchPlugin {
if (!$this->shareeEnumeration) {
$result['wide'] = [];
+ $userResults['wide'] = [];
} else {
$result['wide'] = array_slice($result['wide'], $offset, $limit);
+ $userResults['wide'] = array_slice($userResults['wide'], $offset, $limit);
}
+
if (!$searchResult->hasExactIdMatch($emailType) && filter_var($search, FILTER_VALIDATE_EMAIL)) {
$result['exact'][] = [
'label' => $search,
@@ -189,6 +191,9 @@ class MailPlugin implements ISearchPlugin {
];
}
+ if (!empty($userResults['wide'])) {
+ $searchResult->addResultSet($userType, $userResults['wide'], []);
+ }
$searchResult->addResultSet($emailType, $result['wide'], $result['exact']);
return true;
diff --git a/lib/private/DB/Connection.php b/lib/private/DB/Connection.php
index b9ef447b74b..8a8a2f100ef 100644
--- a/lib/private/DB/Connection.php
+++ b/lib/private/DB/Connection.php
@@ -401,7 +401,7 @@ class Connection extends \Doctrine\DBAL\Connection implements IDBConnection {
}
/**
- * Espace a parameter to be used in a LIKE query
+ * Escape a parameter to be used in a LIKE query
*
* @param string $param
* @return string
diff --git a/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php b/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php
index a32ae4a1827..154f2447c77 100644
--- a/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php
+++ b/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php
@@ -26,6 +26,7 @@ namespace OC\DB\QueryBuilder\ExpressionBuilder;
use Doctrine\DBAL\Query\Expression\ExpressionBuilder as DoctrineExpressionBuilder;
use OC\DB\QueryBuilder\CompositeExpression;
+use OC\DB\QueryBuilder\FunctionBuilder\FunctionBuilder;
use OC\DB\QueryBuilder\Literal;
use OC\DB\QueryBuilder\QueryFunction;
use OC\DB\QueryBuilder\QuoteHelper;
@@ -45,15 +46,20 @@ class ExpressionBuilder implements IExpressionBuilder {
/** @var IDBConnection */
protected $connection;
+ /** @var FunctionBuilder */
+ protected $functionBuilder;
+
/**
* Initializes a new <tt>ExpressionBuilder</tt>.
*
- * @param \OCP\IDBConnection $connection
+ * @param IDBConnection $connection
+ * @param IQueryBuilder $queryBuilder
*/
- public function __construct(IDBConnection $connection) {
+ public function __construct(IDBConnection $connection, IQueryBuilder $queryBuilder) {
$this->connection = $connection;
$this->helper = new QuoteHelper();
$this->expressionBuilder = new DoctrineExpressionBuilder($connection);
+ $this->functionBuilder = $queryBuilder->func();
}
/**
@@ -298,9 +304,7 @@ class ExpressionBuilder implements IExpressionBuilder {
* @since 9.0.0
*/
public function iLike($x, $y, $type = null) {
- $x = $this->helper->quoteColumnName($x);
- $y = $this->helper->quoteColumnName($y);
- return $this->expressionBuilder->comparison("LOWER($x)", 'LIKE', "LOWER($y)");
+ return $this->expressionBuilder->like($this->functionBuilder->lower($x), $this->functionBuilder->lower($y));
}
/**
diff --git a/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php b/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php
index 17f7fd5aa47..aa7ef8e70be 100644
--- a/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php
+++ b/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php
@@ -25,6 +25,7 @@ namespace OC\DB\QueryBuilder\ExpressionBuilder;
use OC\DB\Connection;
+use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
class MySqlExpressionBuilder extends ExpressionBuilder {
@@ -34,9 +35,10 @@ class MySqlExpressionBuilder extends ExpressionBuilder {
/**
* @param \OCP\IDBConnection|Connection $connection
+ * @param IQueryBuilder $queryBuilder
*/
- public function __construct(IDBConnection $connection) {
- parent::__construct($connection);
+ public function __construct(IDBConnection $connection, IQueryBuilder $queryBuilder) {
+ parent::__construct($connection, $queryBuilder);
$params = $connection->getParams();
$this->charset = isset($params['charset']) ? $params['charset'] : 'utf8';
diff --git a/lib/private/DB/QueryBuilder/ExpressionBuilder/SqliteExpressionBuilder.php b/lib/private/DB/QueryBuilder/ExpressionBuilder/SqliteExpressionBuilder.php
index 14d8ad33e40..d9e8a1279cc 100644
--- a/lib/private/DB/QueryBuilder/ExpressionBuilder/SqliteExpressionBuilder.php
+++ b/lib/private/DB/QueryBuilder/ExpressionBuilder/SqliteExpressionBuilder.php
@@ -31,4 +31,8 @@ class SqliteExpressionBuilder extends ExpressionBuilder {
public function like($x, $y, $type = null) {
return parent::like($x, $y, $type) . " ESCAPE '\\'";
}
+
+ public function iLike($x, $y, $type = null) {
+ return $this->like($this->functionBuilder->lower($x), $this->functionBuilder->lower($y), $type);
+ }
}
diff --git a/lib/private/DB/QueryBuilder/FunctionBuilder/FunctionBuilder.php b/lib/private/DB/QueryBuilder/FunctionBuilder/FunctionBuilder.php
index 2789b8cb356..1d745306351 100644
--- a/lib/private/DB/QueryBuilder/FunctionBuilder/FunctionBuilder.php
+++ b/lib/private/DB/QueryBuilder/FunctionBuilder/FunctionBuilder.php
@@ -59,4 +59,8 @@ class FunctionBuilder implements IFunctionBuilder {
public function sum($field) {
return new QueryFunction('SUM(' . $this->helper->quoteColumnName($field) . ')');
}
+
+ public function lower($field) {
+ return new QueryFunction('LOWER(' . $this->helper->quoteColumnName($field) . ')');
+ }
}
diff --git a/lib/private/DB/QueryBuilder/QueryBuilder.php b/lib/private/DB/QueryBuilder/QueryBuilder.php
index 58be4b43255..d6f8bb48acd 100644
--- a/lib/private/DB/QueryBuilder/QueryBuilder.php
+++ b/lib/private/DB/QueryBuilder/QueryBuilder.php
@@ -112,15 +112,15 @@ class QueryBuilder implements IQueryBuilder {
*/
public function expr() {
if ($this->connection instanceof OracleConnection) {
- return new OCIExpressionBuilder($this->connection);
+ return new OCIExpressionBuilder($this->connection, $this);
} else if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
- return new PgSqlExpressionBuilder($this->connection);
+ return new PgSqlExpressionBuilder($this->connection, $this);
} else if ($this->connection->getDatabasePlatform() instanceof MySqlPlatform) {
- return new MySqlExpressionBuilder($this->connection);
+ return new MySqlExpressionBuilder($this->connection, $this);
} else if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) {
- return new SqliteExpressionBuilder($this->connection);
+ return new SqliteExpressionBuilder($this->connection, $this);
} else {
- return new ExpressionBuilder($this->connection);
+ return new ExpressionBuilder($this->connection, $this);
}
}
diff --git a/lib/private/Security/IdentityProof/Key.php b/lib/private/Security/IdentityProof/Key.php
index b01a5c66bb8..4f79dee15db 100644
--- a/lib/private/Security/IdentityProof/Key.php
+++ b/lib/private/Security/IdentityProof/Key.php
@@ -1,4 +1,5 @@
<?php
+declare(strict_types=1);
/**
* @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
*
@@ -33,16 +34,16 @@ class Key {
* @param string $publicKey
* @param string $privateKey
*/
- public function __construct($publicKey, $privateKey) {
+ public function __construct(string $publicKey, string $privateKey) {
$this->publicKey = $publicKey;
$this->privateKey = $privateKey;
}
- public function getPrivate() {
+ public function getPrivate(): string {
return $this->privateKey;
}
- public function getPublic() {
+ public function getPublic(): string {
return $this->publicKey;
}
}
diff --git a/lib/private/Security/IdentityProof/Manager.php b/lib/private/Security/IdentityProof/Manager.php
index 7bfc139b94c..fb27f04d873 100644
--- a/lib/private/Security/IdentityProof/Manager.php
+++ b/lib/private/Security/IdentityProof/Manager.php
@@ -1,4 +1,5 @@
<?php
+declare(strict_types=1);
/**
* @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
*
@@ -59,7 +60,7 @@ class Manager {
*
* @return array [$publicKey, $privateKey]
*/
- protected function generateKeyPair() {
+ protected function generateKeyPair(): array {
$config = [
'digest_alg' => 'sha512',
'private_key_bits' => 2048,
@@ -83,7 +84,7 @@ class Manager {
* @param string $id key id
* @return Key
*/
- protected function generateKey($id) {
+ protected function generateKey(string $id): Key {
list($publicKey, $privateKey) = $this->generateKeyPair();
// Write the private and public key to the disk
@@ -105,7 +106,7 @@ class Manager {
* @param string $id
* @return Key
*/
- protected function retrieveKey($id) {
+ protected function retrieveKey(string $id): Key {
try {
$folder = $this->appData->getFolder($id);
$privateKey = $this->crypto->decrypt(
@@ -124,7 +125,7 @@ class Manager {
* @param IUser $user
* @return Key
*/
- public function getKey(IUser $user) {
+ public function getKey(IUser $user): Key {
$uid = $user->getUID();
return $this->retrieveKey('user-' . $uid);
}
@@ -135,7 +136,7 @@ class Manager {
* @return Key
* @throws \RuntimeException
*/
- public function getSystemKey() {
+ public function getSystemKey(): Key {
$instanceId = $this->config->getSystemValue('instanceid', null);
if ($instanceId === null) {
throw new \RuntimeException('no instance id!');
diff --git a/lib/private/Security/IdentityProof/Signer.php b/lib/private/Security/IdentityProof/Signer.php
index ed2a38f99b8..95546876bdc 100644
--- a/lib/private/Security/IdentityProof/Signer.php
+++ b/lib/private/Security/IdentityProof/Signer.php
@@ -1,4 +1,5 @@
<?php
+declare(strict_types=1);
/**
* @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
*
@@ -57,7 +58,7 @@ class Signer {
* @param IUser $user
* @return array ['message', 'signature']
*/
- public function sign($type, array $data, IUser $user) {
+ public function sign(string $type, array $data, IUser $user): array {
$privateKey = $this->keyManager->getKey($user)->getPrivate();
$data = [
'data' => $data,
@@ -79,7 +80,7 @@ class Signer {
* @param array $data
* @return bool
*/
- public function verify(array $data) {
+ public function verify(array $data): bool {
if(isset($data['message'])
&& isset($data['signature'])
&& isset($data['message']['signer'])
diff --git a/lib/private/Updater.php b/lib/private/Updater.php
index c70113e1c7d..4dc8edc0272 100644
--- a/lib/private/Updater.php
+++ b/lib/private/Updater.php
@@ -395,7 +395,7 @@ class Updater extends BasicEmitter {
if ($appManager->isShipped($app)) {
throw new \UnexpectedValueException('The files of the app "' . $app . '" were not correctly replaced before running the update');
}
- OC_App::disable($app);
+ \OC::$server->getAppManager()->disableApp($app);
$this->emit('\OC\Updater', 'incompatibleAppDisabled', array($app));
}
// no need to disable any app in case this is a non-core upgrade
diff --git a/lib/private/User/Database.php b/lib/private/User/Database.php
index 471ff1f45ae..6e44c902286 100644
--- a/lib/private/User/Database.php
+++ b/lib/private/User/Database.php
@@ -40,6 +40,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
+
/*
*
* The following SQL statement is just a help for developers and will not be
@@ -56,6 +57,7 @@
namespace OC\User;
use OC\Cache\CappedMemoryCache;
+use OC\DB\QueryBuilder\Literal;
use OCP\IUserBackend;
use OCP\Util;
use Symfony\Component\EventDispatcher\EventDispatcher;
@@ -83,6 +85,7 @@ class Database extends Backend implements IUserBackend {
/**
* Create a new user
+ *
* @param string $uid The username of the user to create
* @param string $password The password of the new user
* @return bool
@@ -112,6 +115,7 @@ class Database extends Backend implements IUserBackend {
/**
* delete a user
+ *
* @param string $uid The username of the user to delete
* @return bool
*
@@ -131,6 +135,7 @@ class Database extends Backend implements IUserBackend {
/**
* Set password
+ *
* @param string $uid The username
* @param string $password The new password
* @return bool
@@ -152,6 +157,7 @@ class Database extends Backend implements IUserBackend {
/**
* Set display name
+ *
* @param string $uid The username
* @param string $displayName The new display name
* @return bool
@@ -172,6 +178,7 @@ class Database extends Backend implements IUserBackend {
/**
* get display name of the user
+ *
* @param string $uid user ID of the user
* @return string display name
*/
@@ -189,20 +196,29 @@ class Database extends Backend implements IUserBackend {
* @return array an array of all displayNames (value) and the corresponding uids (key)
*/
public function getDisplayNames($search = '', $limit = null, $offset = null) {
- $parameters = [];
- $searchLike = '';
- if ($search !== '') {
- $parameters[] = '%' . \OC::$server->getDatabaseConnection()->escapeLikeParameter($search) . '%';
- $parameters[] = '%' . \OC::$server->getDatabaseConnection()->escapeLikeParameter($search) . '%';
- $searchLike = ' WHERE LOWER(`displayname`) LIKE LOWER(?) OR '
- . 'LOWER(`uid`) LIKE LOWER(?)';
- }
+ $connection = \OC::$server->getDatabaseConnection();
+
+ $query = $connection->getQueryBuilder();
+
+ $query->select('uid', 'displayname')
+ ->from('users', 'u')
+ ->leftJoin('u', 'preferences', 'p', $query->expr()->andX(
+ $query->expr()->eq('userid', 'uid')),
+ $query->expr()->eq('appid', new Literal('settings')),
+ $query->expr()->eq('configkey', new Literal('email'))
+ )
+ // sqlite doesn't like re-using a single named parameter here
+ ->where($query->expr()->iLike('uid', $query->createPositionalParameter('%' . $connection->escapeLikeParameter($search) . '%')))
+ ->orWhere($query->expr()->iLike('displayname', $query->createPositionalParameter('%' . $connection->escapeLikeParameter($search) . '%')))
+ ->orWhere($query->expr()->iLike('configvalue', $query->createPositionalParameter('%' . $connection->escapeLikeParameter($search) . '%')))
+ ->orderBy($query->func()->lower('displayname'), 'ASC')
+ ->orderBy($query->func()->lower('uid'), 'ASC')
+ ->setMaxResults($limit)
+ ->setFirstResult($offset);
- $displayNames = array();
- $query = \OC_DB::prepare('SELECT `uid`, `displayname` FROM `*PREFIX*users`'
- . $searchLike .' ORDER BY LOWER(`displayname`), LOWER(`uid`) ASC', $limit, $offset);
- $result = $query->execute($parameters);
- while ($row = $result->fetchRow()) {
+ $result = $query->execute();
+ $displayNames = [];
+ while ($row = $result->fetch()) {
$displayNames[$row['uid']] = $row['displayname'];
}
@@ -211,6 +227,7 @@ class Database extends Backend implements IUserBackend {
/**
* Check if the password is correct
+ *
* @param string $uid The username
* @param string $password The password
* @return string
@@ -226,8 +243,8 @@ class Database extends Backend implements IUserBackend {
if ($row) {
$storedHash = $row['password'];
$newHash = '';
- if(\OC::$server->getHasher()->verify($password, $storedHash, $newHash)) {
- if(!empty($newHash)) {
+ if (\OC::$server->getHasher()->verify($password, $storedHash, $newHash)) {
+ if (!empty($newHash)) {
$this->setPassword($uid, $password);
}
return $row['uid'];
@@ -240,15 +257,16 @@ class Database extends Backend implements IUserBackend {
/**
* Load an user in the cache
+ *
* @param string $uid the username
* @return boolean true if user was found, false otherwise
*/
private function loadUser($uid) {
- $uid = (string) $uid;
+ $uid = (string)$uid;
if (!isset($this->cache[$uid])) {
//guests $uid could be NULL or ''
if ($uid === '') {
- $this->cache[$uid]=false;
+ $this->cache[$uid] = false;
return true;
}
@@ -285,26 +303,15 @@ class Database extends Backend implements IUserBackend {
* @return string[] an array of all uids
*/
public function getUsers($search = '', $limit = null, $offset = null) {
- $parameters = [];
- $searchLike = '';
- if ($search !== '') {
- $parameters[] = '%' . \OC::$server->getDatabaseConnection()->escapeLikeParameter($search) . '%';
- $searchLike = ' WHERE LOWER(`uid`) LIKE LOWER(?)';
- $parameters[] = '%' . \OC::$server->getDatabaseConnection()->escapeLikeParameter($search) . '%';
- $searchLike .= ' OR LOWER(`displayname`) LIKE LOWER(?)';
- }
-
- $query = \OC_DB::prepare('SELECT `uid` FROM `*PREFIX*users`' . $searchLike . ' ORDER BY LOWER(`uid`) ASC', $limit, $offset);
- $result = $query->execute($parameters);
- $users = array();
- while ($row = $result->fetchRow()) {
- $users[] = $row['uid'];
- }
- return $users;
+ $users = $this->getDisplayNames($search, $limit, $offset);
+ $userIds = array_keys($users);
+ sort($userIds, SORT_STRING | SORT_FLAG_CASE);
+ return $userIds;
}
/**
* check if a user exists
+ *
* @param string $uid the username
* @return boolean
*/
@@ -315,6 +322,7 @@ class Database extends Backend implements IUserBackend {
/**
* get the user's home directory
+ *
* @param string $uid the username
* @return string|false
*/
@@ -364,14 +372,15 @@ class Database extends Backend implements IUserBackend {
/**
* Backend name to be shown in user management
+ *
* @return string the name of the backend to be shown
*/
- public function getBackendName(){
+ public function getBackendName() {
return 'Database';
}
public static function preLoginNameUsedAsUserName($param) {
- if(!isset($param['uid'])) {
+ if (!isset($param['uid'])) {
throw new \Exception('key uid is expected to be set in $param');
}
diff --git a/lib/private/legacy/app.php b/lib/private/legacy/app.php
index 16e57d43ebc..bc450ffa25d 100644
--- a/lib/private/legacy/app.php
+++ b/lib/private/legacy/app.php
@@ -154,16 +154,9 @@ class OC_App {
\OC::$server->getLogger()->logException($ex);
if (!\OC::$server->getAppManager()->isShipped($app)) {
// Only disable apps which are not shipped
- self::disable($app);
+ \OC::$server->getAppManager()->disableApp($app);
}
}
- if (self::isType($app, array('authentication'))) {
- // since authentication apps affect the "is app enabled for group" check,
- // the enabled apps cache needs to be cleared to make sure that the
- // next time getEnableApps() is called it will also include apps that were
- // enabled for groups
- self::$enabledAppsCache = [];
- }
\OC::$server->getEventLogger()->end('load_app_' . $app);
}
@@ -330,11 +323,6 @@ class OC_App {
}
/**
- * get all enabled apps
- */
- protected static $enabledAppsCache = [];
-
- /**
* Returns apps enabled for the current user.
*
* @param bool $forceRefresh whether to refresh the cache
@@ -393,7 +381,6 @@ class OC_App {
*/
public function enable(string $appId,
array $groups = []) {
- self::$enabledAppsCache = []; // flush
// Check if app is already downloaded
$installer = \OC::$server->query(Installer::class);
@@ -422,30 +409,6 @@ class OC_App {
}
/**
- * This function set an app as disabled in appconfig.
- *
- * @param string $app app
- * @throws Exception
- */
- public static function disable(string $app) {
- // flush
- self::$enabledAppsCache = [];
-
- // run uninstall steps
- $appData = OC_App::getAppInfo($app);
- if (!is_null($appData)) {
- OC_App::executeRepairSteps($app, $appData['repair-steps']['uninstall']);
- }
-
- // emit disable hook - needed anymore ?
- \OC_Hook::emit('OC_App', 'pre_disable', array('app' => $app));
-
- // finally disable it
- $appManager = \OC::$server->getAppManager();
- $appManager->disableApp($app);
- }
-
- /**
* Get the path where to install apps
*
* @return string|false
diff --git a/lib/public/DB/QueryBuilder/IFunctionBuilder.php b/lib/public/DB/QueryBuilder/IFunctionBuilder.php
index 478fd879404..d867d9e5edb 100644
--- a/lib/public/DB/QueryBuilder/IFunctionBuilder.php
+++ b/lib/public/DB/QueryBuilder/IFunctionBuilder.php
@@ -71,4 +71,13 @@ interface IFunctionBuilder {
* @since 12.0.0
*/
public function sum($field);
+
+ /**
+ * Transforms a string field or value to lower case
+ *
+ * @param mixed $field
+ * @return IQueryFunction
+ * @since 14.0.0
+ */
+ public function lower($field);
}
diff --git a/settings/ajax/disableapp.php b/settings/ajax/disableapp.php
index a2f9fa42693..1d9cb984e2a 100644
--- a/settings/ajax/disableapp.php
+++ b/settings/ajax/disableapp.php
@@ -39,6 +39,6 @@ if (!array_key_exists('appid', $_POST)) {
$appIds = (array)$_POST['appid'];
foreach($appIds as $appId) {
$appId = OC_App::cleanAppId($appId);
- OC_App::disable($appId);
+ \OC::$server->getAppManager()->disableApp($appId);
}
OC_JSON::success();
diff --git a/tests/Core/Controller/NavigationControllerTest.php b/tests/Core/Controller/NavigationControllerTest.php
index 1143ed003f0..86173405c1c 100644
--- a/tests/Core/Controller/NavigationControllerTest.php
+++ b/tests/Core/Controller/NavigationControllerTest.php
@@ -23,6 +23,7 @@
namespace Tests\Core\Controller;
use OC\Core\Controller\NavigationController;
+use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\INavigationManager;
use OCP\IRequest;
@@ -126,4 +127,34 @@ class NavigationControllerTest extends TestCase {
}
}
+ public function testGetAppNavigationEtagMatch() {
+ $navigation = [ ['id' => 'files', 'href' => '/index.php/apps/files', 'icon' => 'icon' ] ];
+ $this->request->expects($this->once())
+ ->method('getHeader')
+ ->with('If-None-Match')
+ ->willReturn(md5(json_encode($navigation)));
+ $this->navigationManager->expects($this->once())
+ ->method('getAll')
+ ->with('link')
+ ->willReturn($navigation);
+ $actual = $this->controller->getAppsNavigation();
+ $this->assertInstanceOf(DataResponse::class, $actual);
+ $this->assertEquals(Http::STATUS_NOT_MODIFIED, $actual->getStatus());
+ }
+
+ public function testGetSettingsNavigationEtagMatch() {
+ $navigation = [ ['id' => 'logout', 'href' => '/index.php/apps/files', 'icon' => 'icon' ] ];
+ $this->request->expects($this->once())
+ ->method('getHeader')
+ ->with('If-None-Match')
+ ->willReturn(md5(json_encode([ ['id' => 'logout', 'href' => 'logout', 'icon' => 'icon' ] ])));
+ $this->navigationManager->expects($this->once())
+ ->method('getAll')
+ ->with('settings')
+ ->willReturn($navigation);
+ $actual = $this->controller->getSettingsNavigation();
+ $this->assertInstanceOf(DataResponse::class, $actual);
+ $this->assertEquals(Http::STATUS_NOT_MODIFIED, $actual->getStatus());
+ }
+
}
diff --git a/tests/lib/DB/QueryBuilder/ExpressionBuilderTest.php b/tests/lib/DB/QueryBuilder/ExpressionBuilderTest.php
index ff58b3f6e0b..1bf42d230fe 100644
--- a/tests/lib/DB/QueryBuilder/ExpressionBuilderTest.php
+++ b/tests/lib/DB/QueryBuilder/ExpressionBuilderTest.php
@@ -48,7 +48,9 @@ class ExpressionBuilderTest extends TestCase {
$this->connection = \OC::$server->getDatabaseConnection();
- $this->expressionBuilder = new ExpressionBuilder($this->connection);
+ $queryBuilder = $this->createMock(IQueryBuilder::class);
+
+ $this->expressionBuilder = new ExpressionBuilder($this->connection, $queryBuilder);
$this->doctrineExpressionBuilder = new DoctrineExpressionBuilder($this->connection);
}
diff --git a/tests/lib/DB/QueryBuilder/FunctionBuilderTest.php b/tests/lib/DB/QueryBuilder/FunctionBuilderTest.php
index e89da0fbc92..869faccc5cc 100644
--- a/tests/lib/DB/QueryBuilder/FunctionBuilderTest.php
+++ b/tests/lib/DB/QueryBuilder/FunctionBuilderTest.php
@@ -79,4 +79,14 @@ class FunctionBuilderTest extends TestCase {
$this->assertEquals('oobar', $query->execute()->fetchColumn());
}
+
+ public function testLower() {
+ $query = $this->connection->getQueryBuilder();
+
+ $query->select($query->func()->lower($query->createNamedParameter('FooBar')));
+ $query->from('appconfig')
+ ->setMaxResults(1);
+
+ $this->assertEquals('foobar', $query->execute()->fetchColumn());
+ }
}
diff --git a/tests/lib/User/Backend.php b/tests/lib/User/Backend.php
index 85ccbac913c..1c7d482c480 100644
--- a/tests/lib/User/Backend.php
+++ b/tests/lib/User/Backend.php
@@ -103,15 +103,23 @@ abstract class Backend extends \Test\TestCase {
$name1 = 'foobarbaz';
$name2 = 'bazbarfoo';
$name3 = 'notme';
+ $name4 = 'under_score';
$this->backend->createUser($name1, 'pass1');
$this->backend->createUser($name2, 'pass2');
$this->backend->createUser($name3, 'pass3');
+ $this->backend->createUser($name4, 'pass4');
$result = $this->backend->getUsers('bar');
- $this->assertSame(2, count($result));
+ $this->assertCount(2, $result);
$result = $this->backend->getDisplayNames('bar');
- $this->assertSame(2, count($result));
+ $this->assertCount(2, $result);
+
+ $result = $this->backend->getUsers('under_');
+ $this->assertCount(1, $result);
+
+ $result = $this->backend->getUsers('not_');
+ $this->assertCount(0, $result);
}
}
diff --git a/tests/lib/User/DatabaseTest.php b/tests/lib/User/DatabaseTest.php
index 0e6900651cd..a6fb8047a98 100644
--- a/tests/lib/User/DatabaseTest.php
+++ b/tests/lib/User/DatabaseTest.php
@@ -24,6 +24,7 @@ namespace Test\User;
use OC\HintException;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\GenericEvent;
+use OC\User\User;
/**
* Class DatabaseTest
@@ -113,4 +114,39 @@ class DatabaseTest extends Backend {
$this->backend->createUser($user1, 'pw2');
$this->assertTrue($this->backend->userExists($user1));
}
+
+ public function testSearch() {
+ parent::testSearch();
+
+ $user1 = $this->getUser();
+ $this->backend->createUser($user1, 'pass1');
+
+ $user2 = $this->getUser();
+ $this->backend->createUser($user2, 'pass1');
+
+ $user1Obj = new User($user1, $this->backend);
+ $user2Obj = new User($user2, $this->backend);
+ $emailAddr1 = "$user1@nextcloud.com";
+ $emailAddr2 = "$user2@nextcloud.com";
+
+ $user1Obj->setDisplayName('User 1 Display');
+
+ $result = $this->backend->getDisplayNames('display');
+ $this->assertCount(1, $result);
+
+ $result = $this->backend->getDisplayNames(strtoupper($user1));
+ $this->assertCount(1, $result);
+
+ $user1Obj->setEMailAddress($emailAddr1);
+ $user2Obj->setEMailAddress($emailAddr2);
+
+ $result = $this->backend->getUsers('@nextcloud.com');
+ $this->assertCount(2, $result);
+
+ $result = $this->backend->getDisplayNames('@nextcloud.com');
+ $this->assertCount(2, $result);
+
+ $result = $this->backend->getDisplayNames('@nextcloud.COM');
+ $this->assertCount(2, $result);
+ }
}