summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/dav/lib/Connector/Sabre/FilesPlugin.php9
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php6
-rw-r--r--apps/files/js/filelist.js16
-rw-r--r--apps/files/js/filesummary.js92
-rw-r--r--apps/files/lib/Command/Scan.php27
-rw-r--r--apps/files/tests/js/filelistSpec.js104
-rw-r--r--apps/files/tests/js/filesummarySpec.js74
-rw-r--r--apps/files_sharing/lib/sharedstorage.php10
-rw-r--r--apps/theming/lib/Controller/ThemingController.php24
-rw-r--r--apps/theming/lib/Settings/Admin.php2
-rw-r--r--apps/theming/tests/Controller/ThemingControllerTest.php88
-rw-r--r--apps/theming/tests/Settings/AdminTest.php6
-rw-r--r--build/integration/features/webdav-related.feature18
-rw-r--r--core/Controller/LoginController.php22
-rw-r--r--core/css/multiselect.css2
-rw-r--r--core/css/styles.css3
-rw-r--r--core/js/multiselect.js52
-rw-r--r--core/js/oc-dialogs.js14
-rw-r--r--core/js/sharedialoglinkshareview.js2
-rw-r--r--lib/base.php2
-rw-r--r--lib/private/Files/Cache/Storage.php8
-rw-r--r--lib/private/User/Manager.php10
-rw-r--r--lib/private/legacy/image.php26
-rw-r--r--settings/css/settings.css9
-rw-r--r--settings/js/users/groups.js4
-rw-r--r--settings/js/users/users.js174
-rw-r--r--settings/templates/users/main.php4
-rw-r--r--settings/templates/users/part.createuser.php11
-rw-r--r--settings/templates/users/part.userlist.php8
-rw-r--r--tests/Core/Controller/LoginControllerTest.php76
-rw-r--r--tests/lib/Repair/RepairLegacyStoragesTest.php44
31 files changed, 671 insertions, 276 deletions
diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php
index c472f75b6bf..dd5f958ed4c 100644
--- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php
@@ -324,18 +324,13 @@ class FilesPlugin extends ServerPlugin {
return $displayName;
});
- $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function() use ($node) {
- if ($node->getPath() === '/') {
- return $this->config->getSystemValue('data-fingerprint', '');
- }
- });
-
$propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) {
return json_encode($this->previewManager->isAvailable($node->getFileInfo()));
});
}
- if ($node instanceof \OCA\DAV\Files\FilesHome) {
+ if ($node instanceof \OCA\DAV\Connector\Sabre\Node
+ || $node instanceof \OCA\DAV\Files\FilesHome) {
$propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function() use ($node) {
return $this->config->getSystemValue('data-fingerprint', '');
});
diff --git a/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php
index 6630c027541..e2d63868af0 100644
--- a/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php
@@ -213,7 +213,8 @@ class FilesPluginTest extends TestCase {
$this->assertEquals('http://example.com/', $propFind->get(self::DOWNLOADURL_PROPERTYNAME));
$this->assertEquals('foo', $propFind->get(self::OWNER_ID_PROPERTYNAME));
$this->assertEquals('M. Foo', $propFind->get(self::OWNER_DISPLAY_NAME_PROPERTYNAME));
- $this->assertEquals([self::SIZE_PROPERTYNAME, self::DATA_FINGERPRINT_PROPERTYNAME], $propFind->get404Properties());
+ $this->assertEquals('my_fingerprint', $propFind->get(self::DATA_FINGERPRINT_PROPERTYNAME));
+ $this->assertEquals([self::SIZE_PROPERTYNAME], $propFind->get404Properties());
}
public function testGetPropertiesForFileHome() {
@@ -357,7 +358,8 @@ class FilesPluginTest extends TestCase {
$this->assertEquals(1025, $propFind->get(self::SIZE_PROPERTYNAME));
$this->assertEquals('DWCKMSR', $propFind->get(self::PERMISSIONS_PROPERTYNAME));
$this->assertEquals(null, $propFind->get(self::DOWNLOADURL_PROPERTYNAME));
- $this->assertEquals([self::DOWNLOADURL_PROPERTYNAME, self::DATA_FINGERPRINT_PROPERTYNAME], $propFind->get404Properties());
+ $this->assertEquals('my_fingerprint', $propFind->get(self::DATA_FINGERPRINT_PROPERTYNAME));
+ $this->assertEquals([self::DOWNLOADURL_PROPERTYNAME], $propFind->get404Properties());
}
public function testGetPropertiesForRootDirectory() {
diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js
index f191ade240b..ca41012764a 100644
--- a/apps/files/js/filelist.js
+++ b/apps/files/js/filelist.js
@@ -199,6 +199,7 @@
* @param options.folderDropOptions folder drop options, disabled by default
* @param options.scrollTo name of file to scroll to after the first load
* @param {OC.Files.Client} [options.filesClient] files API client
+ * @param {OC.Backbone.Model} [options.filesConfig] files app configuration
* @private
*/
initialize: function($el, options) {
@@ -212,6 +213,10 @@
this._filesConfig = options.config;
} else if (!_.isUndefined(OCA.Files) && !_.isUndefined(OCA.Files.App)) {
this._filesConfig = OCA.Files.App.getFilesConfig();
+ } else {
+ this._filesConfig = new OC.Backbone.Model({
+ 'showhidden': false
+ });
}
if (options.dragOptions) {
@@ -239,6 +244,7 @@
this._filesConfig.on('change:showhidden', function() {
var showHidden = this.get('showhidden');
self.$el.toggleClass('hide-hidden-files', !showHidden);
+ self.updateSelectionSummary();
if (!showHidden) {
// hiding files could make the page too small, need to try rendering next page
@@ -264,7 +270,7 @@
this.files = [];
this._selectedFiles = {};
- this._selectionSummary = new OCA.Files.FileSummary();
+ this._selectionSummary = new OCA.Files.FileSummary(undefined, {config: this._filesConfig});
// dummy root dir info
this.dirInfo = new OC.Files.FileInfo({});
@@ -2304,7 +2310,7 @@
var $tr = $('<tr class="summary"></tr>');
this.$el.find('tfoot').append($tr);
- return new OCA.Files.FileSummary($tr);
+ return new OCA.Files.FileSummary($tr, {config: this._filesConfig});
},
updateEmptyContent: function() {
var permissions = this.getDirectoryPermissions();
@@ -2451,6 +2457,7 @@
var summary = this._selectionSummary.summary;
var selection;
+ var showHidden = !!this._filesConfig.get('showhidden');
if (summary.totalFiles === 0 && summary.totalDirs === 0) {
this.$el.find('#headerName a.name>span:first').text(t('files','Name'));
this.$el.find('#headerSize a>span:first').text(t('files','Size'));
@@ -2477,6 +2484,11 @@
selection = fileInfo;
}
+ if (!showHidden && summary.totalHidden > 0) {
+ var hiddenInfo = n('files', 'including %n hidden', 'including %n hidden', summary.totalHidden);
+ selection += ' (' + hiddenInfo + ')';
+ }
+
this.$el.find('#headerName a.name>span:first').text(selection);
this.$el.find('#modified a>span:first').text('');
this.$el.find('table').addClass('multiselect');
diff --git a/apps/files/js/filesummary.js b/apps/files/js/filesummary.js
index a4cefe692a8..519718cfc82 100644
--- a/apps/files/js/filesummary.js
+++ b/apps/files/js/filesummary.js
@@ -20,6 +20,15 @@
*/
(function() {
+ var INFO_TEMPLATE =
+ '<span class="info">' +
+ '<span class="dirinfo"></span>' +
+ '<span class="connector"> and </span>' +
+ '<span class="fileinfo"></span>' +
+ '<span class="hiddeninfo"></span>' +
+ '<span class="filter"></span>' +
+ '</span>';
+
/**
* The FileSummary class encapsulates the file summary values and
* the logic to render it in the given container
@@ -28,26 +37,51 @@
* @memberof OCA.Files
*
* @param $tr table row element
+ * @param {OC.Backbone.Model} [options.filesConfig] files app configuration
*/
- var FileSummary = function($tr) {
+ var FileSummary = function($tr, options) {
+ options = options || {};
+ var self = this;
this.$el = $tr;
+ var filesConfig = options.config;
+ if (filesConfig) {
+ this._showHidden = !!filesConfig.get('showhidden');
+ filesConfig.on('change:showhidden', function() {
+ self._showHidden = !!this.get('showhidden');
+ self.update();
+ });
+ }
this.clear();
this.render();
};
FileSummary.prototype = {
+ _showHidden: null,
+
summary: {
totalFiles: 0,
totalDirs: 0,
+ totalHidden: 0,
totalSize: 0,
filter:'',
sumIsPending:false
},
/**
+ * Returns whether the given file info must be hidden
+ *
+ * @param {OC.Files.FileInfo} fileInfo file info
+ *
+ * @return {boolean} true if the file is a hidden file, false otherwise
+ */
+ _isHiddenFile: function(file) {
+ return file.name && file.name.charAt(0) === '.';
+ },
+
+ /**
* Adds file
- * @param file file to add
- * @param update whether to update the display
+ * @param {OC.Files.FileInfo} file file to add
+ * @param {boolean} update whether to update the display
*/
add: function(file, update) {
if (file.name && file.name.toLowerCase().indexOf(this.summary.filter) === -1) {
@@ -59,6 +93,10 @@
else {
this.summary.totalFiles++;
}
+ if (this._isHiddenFile(file)) {
+ this.summary.totalHidden++;
+ }
+
var size = parseInt(file.size, 10) || 0;
if (size >=0) {
this.summary.totalSize += size;
@@ -71,8 +109,8 @@
},
/**
* Removes file
- * @param file file to remove
- * @param update whether to update the display
+ * @param {OC.Files.FileInfo} file file to remove
+ * @param {boolean} update whether to update the display
*/
remove: function(file, update) {
if (file.name && file.name.toLowerCase().indexOf(this.summary.filter) === -1) {
@@ -84,6 +122,9 @@
else {
this.summary.totalFiles--;
}
+ if (this._isHiddenFile(file)) {
+ this.summary.totalHidden--;
+ }
var size = parseInt(file.size, 10) || 0;
if (size >=0) {
this.summary.totalSize -= size;
@@ -111,6 +152,7 @@
var summary = {
totalDirs: 0,
totalFiles: 0,
+ totalHidden: 0,
totalSize: 0,
filter: this.summary.filter,
sumIsPending: false
@@ -127,6 +169,9 @@
else {
summary.totalFiles++;
}
+ if (this._isHiddenFile(file)) {
+ summary.totalHidden++;
+ }
var size = parseInt(file.size, 10) || 0;
if (size >=0) {
summary.totalSize += size;
@@ -154,6 +199,13 @@
this.update();
},
+ _infoTemplate: function(data) {
+ if (!this._infoTemplateCompiled) {
+ this._infoTemplateCompiled = Handlebars.compile(INFO_TEMPLATE);
+ }
+ return this._infoTemplateCompiled(data);
+ },
+
/**
* Renders the file summary element
*/
@@ -171,10 +223,12 @@
var $fileInfo = this.$el.find('.fileinfo');
var $connector = this.$el.find('.connector');
var $filterInfo = this.$el.find('.filter');
+ var $hiddenInfo = this.$el.find('.hiddeninfo');
// Substitute old content with new translations
$dirInfo.html(n('files', '%n folder', '%n folders', this.summary.totalDirs));
$fileInfo.html(n('files', '%n file', '%n files', this.summary.totalFiles));
+ $hiddenInfo.html(' (' + n('files', 'including %n hidden', 'including %n hidden', this.summary.totalHidden) + ')');
var fileSize = this.summary.sumIsPending ? t('files', 'Pending') : OC.Util.humanFileSize(this.summary.totalSize);
this.$el.find('.filesize').html(fileSize);
@@ -194,6 +248,7 @@
if (this.summary.totalDirs > 0 && this.summary.totalFiles > 0) {
$connector.removeClass('hidden');
}
+ $hiddenInfo.toggleClass('hidden', this.summary.totalHidden === 0 || this._showHidden)
if (this.summary.filter === '') {
$filterInfo.html('');
$filterInfo.addClass('hidden');
@@ -206,19 +261,7 @@
if (!this.$el) {
return;
}
- // TODO: ideally this should be separate to a template or something
var summary = this.summary;
- var directoryInfo = n('files', '%n folder', '%n folders', summary.totalDirs);
- var fileInfo = n('files', '%n file', '%n files', summary.totalFiles);
- var filterInfo = '';
- if (this.summary.filter !== '') {
- filterInfo = ' ' + n('files', 'matches \'{filter}\'', 'match \'{filter}\'', summary.totalFiles + summary.totalDirs, {filter: summary.filter});
- }
-
- var infoVars = {
- dirs: '<span class="dirinfo">'+directoryInfo+'</span><span class="connector">',
- files: '</span><span class="fileinfo">'+fileInfo+'</span>'
- };
// don't show the filesize column, if filesize is NaN (e.g. in trashbin)
var fileSize = '';
@@ -227,15 +270,14 @@
fileSize = '<td class="filesize">' + fileSize + '</td>';
}
- var info = t('files', '{dirs} and {files}', infoVars, null, {'escape': false});
-
- var $summary = $('<td><span class="info">'+info+'<span class="filter">'+filterInfo+'</span></span></td>'+fileSize+'<td class="date"></td>');
-
- if (!this.summary.totalFiles && !this.summary.totalDirs) {
- this.$el.addClass('hidden');
- }
-
+ var $summary = $(
+ '<td>' + this._infoTemplate() + '</td>' +
+ fileSize +
+ '<td class="date"></td>'
+ );
+ this.$el.addClass('hidden');
this.$el.append($summary);
+ this.update();
}
};
OCA.Files.FileSummary = FileSummary;
diff --git a/apps/files/lib/Command/Scan.php b/apps/files/lib/Command/Scan.php
index 25933ae25aa..0234fb435a7 100644
--- a/apps/files/lib/Command/Scan.php
+++ b/apps/files/lib/Command/Scan.php
@@ -28,9 +28,11 @@
namespace OCA\Files\Command;
+use Doctrine\DBAL\Connection;
use OC\Core\Command\Base;
use OC\ForbiddenException;
use OCP\Files\StorageNotAvailableException;
+use OCP\IDBConnection;
use OCP\IUserManager;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -106,7 +108,8 @@ class Scan extends Base {
}
protected function scanFiles($user, $path, $verbose, OutputInterface $output, $backgroundScan = false) {
- $scanner = new \OC\Files\Utils\Scanner($user, \OC::$server->getDatabaseConnection(), \OC::$server->getLogger());
+ $connection = $this->reconnectToDatabase($output);
+ $scanner = new \OC\Files\Utils\Scanner($user, $connection, \OC::$server->getLogger());
# check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception
# printout and count
if ($verbose) {
@@ -318,4 +321,26 @@ class Scan extends Base {
return date('H:i:s', $secs);
}
+ /**
+ * @return \OCP\IDBConnection
+ */
+ protected function reconnectToDatabase(OutputInterface $output) {
+ /** @var Connection | IDBConnection $connection*/
+ $connection = \OC::$server->getDatabaseConnection();
+ try {
+ $connection->close();
+ } catch (\Exception $ex) {
+ $output->writeln("<info>Error while disconnecting from database: {$ex->getMessage()}</info>");
+ }
+ while (!$connection->isConnected()) {
+ try {
+ $connection->connect();
+ } catch (\Exception $ex) {
+ $output->writeln("<info>Error while re-connecting to database: {$ex->getMessage()}</info>");
+ sleep(60);
+ }
+ }
+ return $connection;
+ }
+
}
diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js
index cf9f43f2d59..0a4812f3a81 100644
--- a/apps/files/tests/js/filelistSpec.js
+++ b/apps/files/tests/js/filelistSpec.js
@@ -385,8 +385,9 @@ describe('OCA.Files.FileList tests', function() {
$summary = $('#filestable .summary');
expect($summary.hasClass('hidden')).toEqual(false);
// yes, ugly...
- expect($summary.find('.info').text()).toEqual('0 folders and 1 file');
+ expect($summary.find('.fileinfo').text()).toEqual('1 file');
expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(true);
+ expect($summary.find('.connector').hasClass('hidden')).toEqual(true);
expect($summary.find('.fileinfo').hasClass('hidden')).toEqual(false);
expect($summary.find('.filesize').text()).toEqual('12 B');
expect($('#filestable thead th').hasClass('hidden')).toEqual(false);
@@ -456,7 +457,8 @@ describe('OCA.Files.FileList tests', function() {
$summary = $('#filestable .summary');
expect($summary.hasClass('hidden')).toEqual(false);
- expect($summary.find('.info').text()).toEqual('1 folder and 2 files');
+ expect($summary.find('.dirinfo').text()).toEqual('1 folder');
+ expect($summary.find('.fileinfo').text()).toEqual('2 files');
expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(false);
expect($summary.find('.fileinfo').hasClass('hidden')).toEqual(false);
expect($summary.find('.filesize').text()).toEqual('69 KB');
@@ -511,7 +513,8 @@ describe('OCA.Files.FileList tests', function() {
$summary = $('#filestable .summary');
expect($summary.hasClass('hidden')).toEqual(false);
- expect($summary.find('.info').text()).toEqual('1 folder and 1 file');
+ expect($summary.find('.dirinfo').text()).toEqual('1 folder');
+ expect($summary.find('.fileinfo').text()).toEqual('1 file');
expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(false);
expect($summary.find('.fileinfo').hasClass('hidden')).toEqual(false);
expect($summary.find('.filesize').text()).toEqual('57 KB');
@@ -677,12 +680,14 @@ describe('OCA.Files.FileList tests', function() {
deferredRename.resolve(201);
- expect($summary.find('.info').text()).toEqual('1 folder and 3 files');
+ expect($summary.find('.dirinfo').text()).toEqual('1 folder');
+ expect($summary.find('.fileinfo').text()).toEqual('3 files');
});
it('Leaves the summary alone when cancel renaming', function() {
var $summary = $('#filestable .summary');
doCancelRename();
- expect($summary.find('.info').text()).toEqual('1 folder and 3 files');
+ expect($summary.find('.dirinfo').text()).toEqual('1 folder');
+ expect($summary.find('.fileinfo').text()).toEqual('3 files');
});
it('Shows busy state while rename in progress', function() {
var $tr;
@@ -856,11 +861,14 @@ describe('OCA.Files.FileList tests', function() {
});
var $tr = fileList.add(fileData);
- expect($summary.find('.info').text()).toEqual('0 folders and 1 file');
+ expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(true);
+ expect($summary.find('.fileinfo').text()).toEqual('1 file');
var model = fileList.getModelForFile('test file');
model.set({size: '100'});
- expect($summary.find('.info').text()).toEqual('0 folders and 1 file');
+
+ expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(true);
+ expect($summary.find('.fileinfo').text()).toEqual('1 file');
});
})
describe('List rendering', function() {
@@ -877,7 +885,8 @@ describe('OCA.Files.FileList tests', function() {
fileList.setFiles(testFiles);
$summary = $('#filestable .summary');
expect($summary.hasClass('hidden')).toEqual(false);
- expect($summary.find('.info').text()).toEqual('1 folder and 3 files');
+ expect($summary.find('.dirinfo').text()).toEqual('1 folder');
+ expect($summary.find('.fileinfo').text()).toEqual('3 files');
expect($summary.find('.filesize').text()).toEqual('69 KB');
});
it('shows headers, summary and hide empty content message after setting files', function(){
@@ -962,10 +971,12 @@ describe('OCA.Files.FileList tests', function() {
fileList.setFiles([testFiles[0]]);
$summary = $('#filestable .summary');
expect($summary.hasClass('hidden')).toEqual(false);
- expect($summary.find('.info').text()).toEqual('0 folders and 1 file');
+ expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(true);
+ expect($summary.find('.fileinfo').text()).toEqual('1 file');
fileList.remove('unexist.txt');
expect($summary.hasClass('hidden')).toEqual(false);
- expect($summary.find('.info').text()).toEqual('0 folders and 1 file');
+ expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(true);
+ expect($summary.find('.fileinfo').text()).toEqual('1 file');
});
});
describe('Filtered list rendering', function() {
@@ -987,14 +998,18 @@ describe('OCA.Files.FileList tests', function() {
expect($('#fileList tr:not(.hidden)').length).toEqual(3);
expect(fileList.files.length).toEqual(4);
expect($summary.hasClass('hidden')).toEqual(false);
- expect($summary.find('.info').text()).toEqual("1 folder and 2 files match 'e'");
+ expect($summary.find('.dirinfo').text()).toEqual('1 folder');
+ expect($summary.find('.fileinfo').text()).toEqual('2 files');
+ expect($summary.find('.filter').text()).toEqual(" match 'e'");
expect($nofilterresults.hasClass('hidden')).toEqual(true);
fileList.setFilter('ee');
expect($('#fileList tr:not(.hidden)').length).toEqual(1);
expect(fileList.files.length).toEqual(4);
expect($summary.hasClass('hidden')).toEqual(false);
- expect($summary.find('.info').text()).toEqual("0 folders and 1 file matches 'ee'");
+ expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(true);
+ expect($summary.find('.fileinfo').text()).toEqual('1 file');
+ expect($summary.find('.filter').text()).toEqual(" matches 'ee'");
expect($nofilterresults.hasClass('hidden')).toEqual(true);
fileList.setFilter('eee');
@@ -1007,21 +1022,26 @@ describe('OCA.Files.FileList tests', function() {
expect($('#fileList tr:not(.hidden)').length).toEqual(1);
expect(fileList.files.length).toEqual(4);
expect($summary.hasClass('hidden')).toEqual(false);
- expect($summary.find('.info').text()).toEqual("0 folders and 1 file matches 'ee'");
+ expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(true);
+ expect($summary.find('.fileinfo').text()).toEqual('1 file');
+ expect($summary.find('.filter').text()).toEqual(" matches 'ee'");
expect($nofilterresults.hasClass('hidden')).toEqual(true);
fileList.setFilter('e');
expect($('#fileList tr:not(.hidden)').length).toEqual(3);
expect(fileList.files.length).toEqual(4);
expect($summary.hasClass('hidden')).toEqual(false);
- expect($summary.find('.info').text()).toEqual("1 folder and 2 files match 'e'");
+ expect($summary.find('.dirinfo').text()).toEqual('1 folder');
+ expect($summary.find('.fileinfo').text()).toEqual('2 files');
+ expect($summary.find('.filter').text()).toEqual(" match 'e'");
expect($nofilterresults.hasClass('hidden')).toEqual(true);
fileList.setFilter('');
expect($('#fileList tr:not(.hidden)').length).toEqual(4);
expect(fileList.files.length).toEqual(4);
expect($summary.hasClass('hidden')).toEqual(false);
- expect($summary.find('.info').text()).toEqual("1 folder and 3 files");
+ expect($summary.find('.dirinfo').text()).toEqual('1 folder');
+ expect($summary.find('.fileinfo').text()).toEqual('3 files');
expect($nofilterresults.hasClass('hidden')).toEqual(true);
});
it('filters the list of non-rendered rows using filter()', function() {
@@ -1032,7 +1052,9 @@ describe('OCA.Files.FileList tests', function() {
fileList.setFilter('63');
expect($('#fileList tr:not(.hidden)').length).toEqual(1);
expect($summary.hasClass('hidden')).toEqual(false);
- expect($summary.find('.info').text()).toEqual("0 folders and 1 file matches '63'");
+ expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(true);
+ expect($summary.find('.fileinfo').text()).toEqual('1 file');
+ expect($summary.find('.filter').text()).toEqual(" matches '63'");
expect($nofilterresults.hasClass('hidden')).toEqual(true);
});
it('hides the emptyfiles notice when using filter()', function() {
@@ -1654,6 +1676,18 @@ describe('OCA.Files.FileList tests', function() {
$('#fileList tr td.filename input:checkbox').click();
expect($('.select-all').prop('checked')).toEqual(false);
});
+ it('Selecting all files also selects hidden files when invisible', function() {
+ filesConfig.set('showhidden', false);
+ var $tr = fileList.add(new FileInfo({
+ name: '.hidden',
+ type: 'dir',
+ mimetype: 'httpd/unix-directory',
+ size: 150
+ }));
+ $('.select-all').click();
+ expect($tr.find('td.filename input:checkbox').prop('checked')).toEqual(true);
+ expect(_.pluck(fileList.getSelectedFiles(), 'name')).toContain('.hidden');
+ });
it('Clicking "select all" will select/deselect all files', function() {
fileList.setFiles(generateFiles(0, 41));
$('.select-all').click();
@@ -1731,6 +1765,44 @@ describe('OCA.Files.FileList tests', function() {
fileList.findFileEl('One.txt').find('input:checkbox').click().click();
expect($summary.text()).toEqual('Name');
});
+ it('Displays the number of hidden files in selection summary if hidden files are invisible', function() {
+ filesConfig.set('showhidden', false);
+ var $tr = fileList.add(new FileInfo({
+ name: '.hidden',
+ type: 'dir',
+ mimetype: 'httpd/unix-directory',
+ size: 150
+ }));
+ $('.select-all').click();
+ var $summary = $('#headerName a.name>span:first');
+ expect($summary.text()).toEqual('2 folders and 3 files (including 1 hidden)');
+ });
+ it('Does not displays the number of hidden files in selection summary if hidden files are visible', function() {
+ filesConfig.set('showhidden', true);
+ var $tr = fileList.add(new FileInfo({
+ name: '.hidden',
+ type: 'dir',
+ mimetype: 'httpd/unix-directory',
+ size: 150
+ }));
+ $('.select-all').click();
+ var $summary = $('#headerName a.name>span:first');
+ expect($summary.text()).toEqual('2 folders and 3 files');
+ });
+ it('Toggling hidden file visibility updates selection summary', function() {
+ filesConfig.set('showhidden', false);
+ var $tr = fileList.add(new FileInfo({
+ name: '.hidden',
+ type: 'dir',
+ mimetype: 'httpd/unix-directory',
+ size: 150
+ }));
+ $('.select-all').click();
+ var $summary = $('#headerName a.name>span:first');
+ expect($summary.text()).toEqual('2 folders and 3 files (including 1 hidden)');
+ filesConfig.set('showhidden', true);
+ expect($summary.text()).toEqual('2 folders and 3 files');
+ });
it('Select/deselect files shows/hides file actions', function() {
var $actions = $('#headerName .selectedActions');
var $checkbox = fileList.findFileEl('One.txt').find('input:checkbox');
diff --git a/apps/files/tests/js/filesummarySpec.js b/apps/files/tests/js/filesummarySpec.js
index ec94c28acb6..e3f24d9ad43 100644
--- a/apps/files/tests/js/filesummarySpec.js
+++ b/apps/files/tests/js/filesummarySpec.js
@@ -39,7 +39,8 @@ describe('OCA.Files.FileSummary tests', function() {
totalSize: 256000
});
expect($container.hasClass('hidden')).toEqual(false);
- expect($container.find('.info').text()).toEqual('5 folders and 2 files');
+ expect($container.find('.dirinfo').text()).toEqual('5 folders');
+ expect($container.find('.fileinfo').text()).toEqual('2 files');
expect($container.find('.filesize').text()).toEqual('250 KB');
});
it('hides summary when no files or folders', function() {
@@ -62,7 +63,8 @@ describe('OCA.Files.FileSummary tests', function() {
s.add({type: 'dir', size: 100});
s.update();
expect($container.hasClass('hidden')).toEqual(false);
- expect($container.find('.info').text()).toEqual('6 folders and 3 files');
+ expect($container.find('.dirinfo').text()).toEqual('6 folders');
+ expect($container.find('.fileinfo').text()).toEqual('3 files');
expect($container.find('.filesize').text()).toEqual('500 KB');
expect(s.summary.totalDirs).toEqual(6);
expect(s.summary.totalFiles).toEqual(3);
@@ -79,7 +81,8 @@ describe('OCA.Files.FileSummary tests', function() {
s.remove({type: 'dir', size: 100});
s.update();
expect($container.hasClass('hidden')).toEqual(false);
- expect($container.find('.info').text()).toEqual('4 folders and 1 file');
+ expect($container.find('.dirinfo').text()).toEqual('4 folders');
+ expect($container.find('.fileinfo').text()).toEqual('1 file');
expect($container.find('.filesize').text()).toEqual('125 KB');
expect(s.summary.totalDirs).toEqual(4);
expect(s.summary.totalFiles).toEqual(1);
@@ -95,7 +98,9 @@ describe('OCA.Files.FileSummary tests', function() {
filter: 'foo'
});
expect($container.hasClass('hidden')).toEqual(false);
- expect($container.find('.info').text()).toEqual('5 folders and 2 files match \'foo\'');
+ expect($container.find('.dirinfo').text()).toEqual('5 folders');
+ expect($container.find('.fileinfo').text()).toEqual('2 files');
+ expect($container.find('.filter').text()).toEqual(' match \'foo\'');
expect($container.find('.filesize').text()).toEqual('250 KB');
});
it('hides filtered summary when no files or folders', function() {
@@ -122,7 +127,9 @@ describe('OCA.Files.FileSummary tests', function() {
s.add({name: 'foo', type: 'dir', size: 102});
s.update();
expect($container.hasClass('hidden')).toEqual(false);
- expect($container.find('.info').text()).toEqual('6 folders and 3 files match \'foo\'');
+ expect($container.find('.dirinfo').text()).toEqual('6 folders');
+ expect($container.find('.fileinfo').text()).toEqual('3 files');
+ expect($container.find('.filter').text()).toEqual(' match \'foo\'');
expect($container.find('.filesize').text()).toEqual('500 KB');
expect(s.summary.totalDirs).toEqual(6);
expect(s.summary.totalFiles).toEqual(3);
@@ -142,7 +149,9 @@ describe('OCA.Files.FileSummary tests', function() {
s.remove({name: 'foo', type: 'dir', size: 98});
s.update();
expect($container.hasClass('hidden')).toEqual(false);
- expect($container.find('.info').text()).toEqual('4 folders and 1 file match \'foo\'');
+ expect($container.find('.dirinfo').text()).toEqual('4 folders');
+ expect($container.find('.fileinfo').text()).toEqual('1 file');
+ expect($container.find('.filter').text()).toEqual(' match \'foo\'');
expect($container.find('.filesize').text()).toEqual('125 KB');
expect(s.summary.totalDirs).toEqual(4);
expect(s.summary.totalFiles).toEqual(1);
@@ -158,7 +167,8 @@ describe('OCA.Files.FileSummary tests', function() {
s.add({type: 'dir', size: -1});
s.update();
expect($container.hasClass('hidden')).toEqual(false);
- expect($container.find('.info').text()).toEqual('1 folder and 0 files');
+ expect($container.find('.dirinfo').text()).toEqual('1 folder');
+ expect($container.find('.fileinfo').hasClass('hidden')).toEqual(true);
expect($container.find('.filesize').text()).toEqual('Pending');
expect(s.summary.totalDirs).toEqual(1);
expect(s.summary.totalFiles).toEqual(0);
@@ -175,10 +185,56 @@ describe('OCA.Files.FileSummary tests', function() {
s.remove({type: 'dir', size: -1});
s.update();
expect($container.hasClass('hidden')).toEqual(true);
- expect($container.find('.info').text()).toEqual('0 folders and 0 files');
- expect($container.find('.filesize').text()).toEqual('0 B');
expect(s.summary.totalDirs).toEqual(0);
expect(s.summary.totalFiles).toEqual(0);
expect(s.summary.totalSize).toEqual(0);
});
+ describe('hidden files', function() {
+ var config;
+ var summary;
+
+ beforeEach(function() {
+ config = new OC.Backbone.Model();
+ summary = new FileSummary($container, {
+ config: config
+ });
+ });
+
+ it('renders hidden count section when hidden files are hidden', function() {
+ config.set('showhidden', false);
+ summary.add({name: 'abc', type: 'file', size: 256000});
+ summary.add({name: 'def', type: 'dir', size: 100});
+ summary.add({name: '.hidden', type: 'dir', size: 512000});
+ summary.update();
+ expect($container.hasClass('hidden')).toEqual(false);
+ expect($container.find('.dirinfo').text()).toEqual('2 folders');
+ expect($container.find('.fileinfo').text()).toEqual('1 file');
+ expect($container.find('.hiddeninfo').hasClass('hidden')).toEqual(false);
+ expect($container.find('.hiddeninfo').text()).toEqual(' (including 1 hidden)');
+ expect($container.find('.filesize').text()).toEqual('750 KB');
+ });
+ it('does not render hidden count section when hidden files exist but are visible', function() {
+ config.set('showhidden', true);
+ summary.add({name: 'abc', type: 'file', size: 256000});
+ summary.add({name: 'def', type: 'dir', size: 100});
+ summary.add({name: '.hidden', type: 'dir', size: 512000});
+ summary.update();
+ expect($container.hasClass('hidden')).toEqual(false);
+ expect($container.find('.dirinfo').text()).toEqual('2 folders');
+ expect($container.find('.fileinfo').text()).toEqual('1 file');
+ expect($container.find('.hiddeninfo').hasClass('hidden')).toEqual(true);
+ expect($container.find('.filesize').text()).toEqual('750 KB');
+ });
+ it('does not render hidden count section when no hidden files exist', function() {
+ config.set('showhidden', false);
+ summary.add({name: 'abc', type: 'file', size: 256000});
+ summary.add({name: 'def', type: 'dir', size: 100});
+ summary.update();
+ expect($container.hasClass('hidden')).toEqual(false);
+ expect($container.find('.dirinfo').text()).toEqual('1 folder');
+ expect($container.find('.fileinfo').text()).toEqual('1 file');
+ expect($container.find('.hiddeninfo').hasClass('hidden')).toEqual(true);
+ expect($container.find('.filesize').text()).toEqual('250 KB');
+ });
+ });
});
diff --git a/apps/files_sharing/lib/sharedstorage.php b/apps/files_sharing/lib/sharedstorage.php
index 3ceca430424..e1875fe2394 100644
--- a/apps/files_sharing/lib/sharedstorage.php
+++ b/apps/files_sharing/lib/sharedstorage.php
@@ -106,6 +106,16 @@ class Shared extends \OC\Files\Storage\Wrapper\Jail implements ISharedStorage {
}
/**
+ * @inheritdoc
+ */
+ public function instanceOfStorage($class) {
+ if (in_array($class, ['\OC\Files\Storage\Home', '\OC\Files\ObjectStore\HomeObjectStoreStorage'])) {
+ return false;
+ }
+ return parent::instanceOfStorage($class);
+ }
+
+ /**
* @return string
*/
public function getShareId() {
diff --git a/apps/theming/lib/Controller/ThemingController.php b/apps/theming/lib/Controller/ThemingController.php
index fbb4c904773..b4e3a95710f 100644
--- a/apps/theming/lib/Controller/ThemingController.php
+++ b/apps/theming/lib/Controller/ThemingController.php
@@ -304,6 +304,13 @@ class ThemingController extends Controller {
$responseCss = '';
$color = $this->config->getAppValue($this->appName, 'color');
$elementColor = $this->util->elementColor($color);
+
+ if($this->util->invertTextColor($color)) {
+ $textColor = '#000000';
+ } else {
+ $textColor = '#ffffff';
+ }
+
if($color !== '') {
$responseCss .= sprintf(
'#body-user #header,#body-settings #header,#body-public #header,#body-login,.searchbox input[type="search"]:focus,.searchbox input[type="search"]:active,.searchbox input[type="search"]:valid {background-color: %s}' . "\n",
@@ -321,19 +328,26 @@ class ThemingController extends Controller {
'background-image: url(\'data:image/svg+xml;base64,'.$this->util->generateRadioButton($elementColor).'\');' .
"}\n";
$responseCss .= '.primary, input[type="submit"].primary, input[type="button"].primary, button.primary, .button.primary,' .
- '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active,' .
- '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' .
- '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' .
- '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' .
+ '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active {' .
'border: 1px solid '.$elementColor.';'.
'background-color: '.$elementColor.';'.
- 'opacity: 0.8' .
+ 'opacity: 0.8;' .
+ 'color: ' . $textColor . ';'.
"}\n" .
'.primary:hover, input[type="submit"].primary:hover, input[type="button"].primary:hover, button.primary:hover, .button.primary:hover,' .
'.primary:focus, input[type="submit"].primary:focus, input[type="button"].primary:focus, button.primary:focus, .button.primary:focus {' .
'border: 1px solid '.$elementColor.';'.
'background-color: '.$elementColor.';'.
'opacity: 1.0;' .
+ 'color: ' . $textColor . ';'.
+ "}\n" .
+ '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' .
+ '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' .
+ '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' .
+ 'border: 1px solid '.$elementColor.';'.
+ 'background-color: '.$elementColor.';'.
+ 'opacity: 0.4;' .
+ 'color: '.$textColor.';'.
"}\n";
$responseCss .= '.ui-widget-header { border: 1px solid ' . $color . '; background: '. $color . '; color: #ffffff;' . "}\n";
$responseCss .= '.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active {' .
diff --git a/apps/theming/lib/Settings/Admin.php b/apps/theming/lib/Settings/Admin.php
index afd74ced217..22ab5650e5b 100644
--- a/apps/theming/lib/Settings/Admin.php
+++ b/apps/theming/lib/Settings/Admin.php
@@ -61,7 +61,7 @@ class Admin implements ISettings {
$theme = $this->config->getSystemValue('theme', '');
if ($theme !== '') {
$themable = false;
- $errorMessage = $this->l->t('You already use a custom theme');
+ $errorMessage = $this->l->t('You are already using a custom theme');
}
$parameters = [
diff --git a/apps/theming/tests/Controller/ThemingControllerTest.php b/apps/theming/tests/Controller/ThemingControllerTest.php
index d053d8c1a1c..193e0bdcb4b 100644
--- a/apps/theming/tests/Controller/ThemingControllerTest.php
+++ b/apps/theming/tests/Controller/ThemingControllerTest.php
@@ -437,19 +437,26 @@ class ThemingControllerTest extends TestCase {
'background-image: url(\'data:image/svg+xml;base64,'.$this->util->generateRadioButton($color).'\');' .
"}\n";
$expectedData .= '.primary, input[type="submit"].primary, input[type="button"].primary, button.primary, .button.primary,' .
- '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active,' .
- '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' .
- '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' .
- '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' .
- 'border: 1px solid '.$color .';'.
+ '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active {' .
+ 'border: 1px solid '.$color.';'.
'background-color: '.$color.';'.
- 'opacity: 0.8' .
+ 'opacity: 0.8;' .
+ 'color: #ffffff;'.
"}\n" .
'.primary:hover, input[type="submit"].primary:hover, input[type="button"].primary:hover, button.primary:hover, .button.primary:hover,' .
'.primary:focus, input[type="submit"].primary:focus, input[type="button"].primary:focus, button.primary:focus, .button.primary:focus {' .
'border: 1px solid '.$color.';'.
'background-color: '.$color.';'.
'opacity: 1.0;' .
+ 'color: #ffffff;'.
+ "}\n" .
+ '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' .
+ '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' .
+ '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' .
+ 'border: 1px solid '.$color.';'.
+ 'background-color: '.$color.';'.
+ 'opacity: 0.4;' .
+ 'color: #ffffff;'.
"}\n";
$expectedData .= '.ui-widget-header { border: 1px solid ' . $color . '; background: '. $color . '; color: #ffffff;' . "}\n";
$expectedData .= '.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active {' .
@@ -520,19 +527,26 @@ class ThemingControllerTest extends TestCase {
'background-image: url(\'data:image/svg+xml;base64,'.$this->util->generateRadioButton('#555555').'\');' .
"}\n";
$expectedData .= '.primary, input[type="submit"].primary, input[type="button"].primary, button.primary, .button.primary,' .
- '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active,' .
- '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' .
- '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' .
- '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' .
- 'border: 1px solid #555555;'.
- 'background-color: #555555;'.
- 'opacity: 0.8' .
+ '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active {' .
+ 'border: 1px solid '.$elementColor.';'.
+ 'background-color: '.$elementColor.';'.
+ 'opacity: 0.8;' .
+ 'color: #000000;'.
"}\n" .
'.primary:hover, input[type="submit"].primary:hover, input[type="button"].primary:hover, button.primary:hover, .button.primary:hover,' .
'.primary:focus, input[type="submit"].primary:focus, input[type="button"].primary:focus, button.primary:focus, .button.primary:focus {' .
- 'border: 1px solid #555555;'.
- 'background-color: #555555;'.
+ 'border: 1px solid '.$elementColor.';'.
+ 'background-color: '.$elementColor.';'.
'opacity: 1.0;' .
+ 'color: #000000;'.
+ "}\n" .
+ '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' .
+ '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' .
+ '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' .
+ 'border: 1px solid '.$elementColor.';'.
+ 'background-color: '.$elementColor.';'.
+ 'opacity: 0.4;' .
+ 'color: #000000;'.
"}\n";
$expectedData .= '.ui-widget-header { border: 1px solid ' . $color . '; background: '. $color . '; color: #ffffff;' . "}\n";
$expectedData .= '.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active {' .
@@ -689,19 +703,26 @@ class ThemingControllerTest extends TestCase {
'background-image: url(\'data:image/svg+xml;base64,'.$this->util->generateRadioButton($color).'\');' .
"}\n";
$expectedData .= '.primary, input[type="submit"].primary, input[type="button"].primary, button.primary, .button.primary,' .
- '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active,' .
- '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' .
- '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' .
- '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' .
- 'border: 1px solid '.$color .';'.
+ '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active {' .
+ 'border: 1px solid '.$color.';'.
'background-color: '.$color.';'.
- 'opacity: 0.8' .
+ 'opacity: 0.8;' .
+ 'color: #ffffff;'.
"}\n" .
'.primary:hover, input[type="submit"].primary:hover, input[type="button"].primary:hover, button.primary:hover, .button.primary:hover,' .
'.primary:focus, input[type="submit"].primary:focus, input[type="button"].primary:focus, button.primary:focus, .button.primary:focus {' .
'border: 1px solid '.$color.';'.
'background-color: '.$color.';'.
'opacity: 1.0;' .
+ 'color: #ffffff;'.
+ "}\n" .
+ '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' .
+ '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' .
+ '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' .
+ 'border: 1px solid '.$color.';'.
+ 'background-color: '.$color.';'.
+ 'opacity: 0.4;' .
+ 'color: #ffffff;'.
"}\n";
$expectedData .= '.ui-widget-header { border: 1px solid ' . $color . '; background: '. $color . '; color: #ffffff;' . "}\n";
$expectedData .= '.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active {' .
@@ -789,19 +810,26 @@ class ThemingControllerTest extends TestCase {
'background-image: url(\'data:image/svg+xml;base64,'.$this->util->generateRadioButton('#555555').'\');' .
"}\n";
$expectedData .= '.primary, input[type="submit"].primary, input[type="button"].primary, button.primary, .button.primary,' .
- '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active,' .
- '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' .
- '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' .
- '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' .
- 'border: 1px solid #555555;'.
- 'background-color: #555555;'.
- 'opacity: 0.8' .
+ '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active {' .
+ 'border: 1px solid '.$elementColor.';'.
+ 'background-color: '.$elementColor.';'.
+ 'opacity: 0.8;' .
+ 'color: #000000;'.
"}\n" .
'.primary:hover, input[type="submit"].primary:hover, input[type="button"].primary:hover, button.primary:hover, .button.primary:hover,' .
'.primary:focus, input[type="submit"].primary:focus, input[type="button"].primary:focus, button.primary:focus, .button.primary:focus {' .
- 'border: 1px solid #555555;'.
- 'background-color: #555555;'.
+ 'border: 1px solid '.$elementColor.';'.
+ 'background-color: '.$elementColor.';'.
'opacity: 1.0;' .
+ 'color: #000000;'.
+ "}\n" .
+ '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' .
+ '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' .
+ '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' .
+ 'border: 1px solid '.$elementColor.';'.
+ 'background-color: '.$elementColor.';'.
+ 'opacity: 0.4;' .
+ 'color: #000000;'.
"}\n";
$expectedData .= '.ui-widget-header { border: 1px solid ' . $color . '; background: '. $color . '; color: #ffffff;' . "}\n";
$expectedData .= '.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active {' .
diff --git a/apps/theming/tests/Settings/AdminTest.php b/apps/theming/tests/Settings/AdminTest.php
index 73339cf86b7..d4f5490d352 100644
--- a/apps/theming/tests/Settings/AdminTest.php
+++ b/apps/theming/tests/Settings/AdminTest.php
@@ -112,8 +112,8 @@ class AdminTest extends TestCase {
$this->l10n
->expects($this->once())
->method('t')
- ->with('You already use a custom theme')
- ->willReturn('You already use a custom theme');
+ ->with('You are already using a custom theme')
+ ->willReturn('You are already using a custom theme');
$this->themingDefaults
->expects($this->once())
->method('getEntity')
@@ -137,7 +137,7 @@ class AdminTest extends TestCase {
->willReturn('/my/route');
$params = [
'themable' => false,
- 'errorMessage' => 'You already use a custom theme',
+ 'errorMessage' => 'You are already using a custom theme',
'name' => 'MyEntity',
'url' => 'https://example.com',
'slogan' => 'MySlogan',
diff --git a/build/integration/features/webdav-related.feature b/build/integration/features/webdav-related.feature
index c49db4f8a5d..a59d65a2674 100644
--- a/build/integration/features/webdav-related.feature
+++ b/build/integration/features/webdav-related.feature
@@ -59,6 +59,24 @@ Feature: webdav-related
|{DAV:}quota-available-bytes|
And the single response should contain a property "{DAV:}quota-available-bytes" with value "10485421"
+ Scenario: Uploading a file as recipient using webdav having quota
+ Given using dav path "remote.php/webdav"
+ And As an "admin"
+ And user "user0" exists
+ And user "user1" exists
+ And user "user0" has a quota of "10 MB"
+ And user "user1" has a quota of "10 MB"
+ And As an "user1"
+ And user "user1" created a folder "/testquota"
+ And as "user1" creating a share with
+ | path | testquota |
+ | shareType | 0 |
+ | permissions | 31 |
+ | shareWith | user0 |
+ And As an "user0"
+ When User "user0" uploads file "data/textfile.txt" to "/testquota/asdf.txt"
+ Then the HTTP status code should be "201"
+
Scenario: download a public shared file with range
Given user "user0" exists
And As an "user0"
diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php
index 67e1e215289..b686b34b2ce 100644
--- a/core/Controller/LoginController.php
+++ b/core/Controller/LoginController.php
@@ -242,12 +242,26 @@ class LoginController extends Controller {
if ($this->twoFactorManager->isTwoFactorAuthenticated($loginResult)) {
$this->twoFactorManager->prepareTwoFactorLogin($loginResult);
+
+ $providers = $this->twoFactorManager->getProviders($loginResult);
+ if (count($providers) === 1) {
+ // Single provider, hence we can redirect to that provider's challenge page directly
+ /* @var $provider IProvider */
+ $provider = array_pop($providers);
+ $url = 'core.TwoFactorChallenge.showChallenge';
+ $urlParams = [
+ 'challengeProviderId' => $provider->getId(),
+ ];
+ } else {
+ $url = 'core.TwoFactorChallenge.selectChallenge';
+ $urlParams = [];
+ }
+
if (!is_null($redirect_url)) {
- return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge', [
- 'redirect_url' => $redirect_url
- ]));
+ $urlParams['redirect_url'] = $redirect_url;
}
- return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge'));
+
+ return new RedirectResponse($this->urlGenerator->linkToRoute($url, $urlParams));
}
return $this->generateRedirect($redirect_url);
diff --git a/core/css/multiselect.css b/core/css/multiselect.css
index ef56044fd05..cc1d6a3b468 100644
--- a/core/css/multiselect.css
+++ b/core/css/multiselect.css
@@ -42,6 +42,8 @@ ul.multiselectoptions > li input[type='checkbox']+label {
width: 100%;
padding: 5px 27px;
margin-left: -27px; /* to have area around checkbox clickable as well */
+ text-overflow: ellipsis;
+ overflow: hidden;
}
ul.multiselectoptions > li input[type='checkbox']:checked+label {
font-weight: bold;
diff --git a/core/css/styles.css b/core/css/styles.css
index c2b883e4a36..25bc2d086d5 100644
--- a/core/css/styles.css
+++ b/core/css/styles.css
@@ -301,7 +301,8 @@ body {
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=70)";
opacity: .7;
}
-#body-login input[type="password"] {
+#body-login input[type="password"],
+#body-login input[name="adminpass-clone"] {
padding-right: 40px;
box-sizing: border-box;
min-width: 269px;
diff --git a/core/js/multiselect.js b/core/js/multiselect.js
index 71cf3e10a69..bdf420a2f7f 100644
--- a/core/js/multiselect.js
+++ b/core/js/multiselect.js
@@ -32,7 +32,7 @@
'onuncheck':false,
'minWidth': 'default;'
};
- var slideDuration = 200;
+ var slideDuration = 0;
$(this).attr('data-msid', multiSelectId);
$.extend(settings,options);
$.each(this.children(),function(i,option) {
@@ -75,6 +75,26 @@
var self = this;
self.menuDirection = 'down';
+
+ function closeDropDown() {
+ if(!button.parent().data('preventHide')) {
+ // How can I save the effect in a var?
+ if(self.menuDirection === 'down') {
+ button.parent().children('ul').slideUp(slideDuration,function() {
+ button.parent().children('ul').remove();
+ button.removeClass('active down');
+ $(self).trigger($.Event('dropdownclosed', settings));
+ });
+ } else {
+ button.parent().children('ul').fadeOut(slideDuration,function() {
+ button.parent().children('ul').remove();
+ button.removeClass('active up');
+ $(self).trigger($.Event('dropdownclosed', settings));
+ });
+ }
+ }
+ }
+
button.click(function(event){
var button=$(this);
@@ -83,21 +103,20 @@
button.parent().children('ul').slideUp(slideDuration,function() {
button.parent().children('ul').remove();
button.removeClass('active down');
+ $(self).trigger($.Event('dropdownclosed', settings));
});
} else {
button.parent().children('ul').fadeOut(slideDuration,function() {
button.parent().children('ul').remove();
button.removeClass('active up');
+ $(self).trigger($.Event('dropdownclosed', settings));
});
}
return;
}
+ // tell other lists to shut themselves
var lists=$('ul.multiselectoptions');
- lists.slideUp(slideDuration,function(){
- lists.remove();
- $('div.multiselect').removeClass('active');
- button.addClass('active');
- });
+ lists.trigger($.Event('shut'));
button.addClass('active');
event.stopPropagation();
var options=$(this).parent().next().children();
@@ -309,29 +328,16 @@
list.detach().insertBefore($(this));
list.addClass('up');
button.addClass('up');
- list.fadeIn();
+ list.show();
self.menuDirection = 'up';
}
list.click(function(event) {
event.stopPropagation();
});
+ list.one('shut', closeDropDown);
});
- $(window).click(function() {
- if(!button.parent().data('preventHide')) {
- // How can I save the effect in a var?
- if(self.menuDirection === 'down') {
- button.parent().children('ul').slideUp(slideDuration,function() {
- button.parent().children('ul').remove();
- button.removeClass('active down');
- });
- } else {
- button.parent().children('ul').fadeOut(slideDuration,function() {
- button.parent().children('ul').remove();
- button.removeClass('active up');
- });
- }
- }
- });
+
+ $(window).click(closeDropDown);
return span;
};
diff --git a/core/js/oc-dialogs.js b/core/js/oc-dialogs.js
index b77063a9eae..75c8ef9020e 100644
--- a/core/js/oc-dialogs.js
+++ b/core/js/oc-dialogs.js
@@ -218,6 +218,13 @@ var OCdialogs = {
self.$filePicker = null;
}
});
+
+ // We can access primary class only from oc-dialog.
+ // Hence this is one of the approach to get the choose button.
+ var getOcDialog = self.$filePicker.closest('.oc-dialog');
+ var buttonEnableDisable = getOcDialog.find('.primary');
+ buttonEnableDisable.prop("disabled", "true");
+
if (!OC.Util.hasSVGSupport()) {
OC.Util.replaceSVG(self.$filePicker.parent());
}
@@ -812,18 +819,25 @@ var OCdialogs = {
var self = event.data;
var dir = $(event.target).data('dir');
self._fillFilePicker(dir);
+ var getOcDialog = this.closest('.oc-dialog');
+ var buttonEnableDisable = $('.primary', getOcDialog);
+ buttonEnableDisable.prop("disabled", true);
},
/**
* handle clicks made in the filepicker
*/
_handlePickerClick:function(event, $element) {
+ var getOcDialog = this.$filePicker.closest('.oc-dialog');
+ var buttonEnableDisable = getOcDialog.find('.primary');
if ($element.data('type') === 'file') {
if (this.$filePicker.data('multiselect') !== true || !event.ctrlKey) {
this.$filelist.find('.filepicker_element_selected').removeClass('filepicker_element_selected');
}
$element.toggleClass('filepicker_element_selected');
+ buttonEnableDisable.prop("disabled", false);
} else if ( $element.data('type') === 'dir' ) {
this._fillFilePicker(this.$filePicker.data('path') + '/' + $element.data('entryname'));
+ buttonEnableDisable.prop("disabled", true);
}
}
};
diff --git a/core/js/sharedialoglinkshareview.js b/core/js/sharedialoglinkshareview.js
index 83bf7979000..1d6a0f03d4d 100644
--- a/core/js/sharedialoglinkshareview.js
+++ b/core/js/sharedialoglinkshareview.js
@@ -330,7 +330,7 @@
publicUpload: publicUpload && isLinkShare,
publicUploadChecked: publicUploadChecked,
hideFileListChecked: hideFileListChecked,
- publicUploadLabel: t('core', 'Allow editing'),
+ publicUploadLabel: t('core', 'Allow upload and editing'),
hideFileListLabel: t('core', 'Hide file listing'),
mailPublicNotificationEnabled: isLinkShare && this.configModel.isMailPublicNotificationEnabled(),
mailPrivatePlaceholder: t('core', 'Email link to person'),
diff --git a/lib/base.php b/lib/base.php
index a69a4dffef8..68eab7c4acf 100644
--- a/lib/base.php
+++ b/lib/base.php
@@ -268,7 +268,7 @@ class OC {
if (OC::$CLI) {
throw new Exception('Not installed');
} else {
- $url = 'http://' . $_SERVER['SERVER_NAME'] . OC::$WEBROOT . '/index.php';
+ $url = OC::$WEBROOT . '/index.php';
header('Location: ' . $url);
}
exit();
diff --git a/lib/private/Files/Cache/Storage.php b/lib/private/Files/Cache/Storage.php
index 99b127ab220..8a076084ac5 100644
--- a/lib/private/Files/Cache/Storage.php
+++ b/lib/private/Files/Cache/Storage.php
@@ -57,15 +57,15 @@ class Storage {
$this->storageId = self::adjustStorageId($this->storageId);
if ($row = self::getStorageById($this->storageId)) {
- $this->numericId = $row['numeric_id'];
+ $this->numericId = (int)$row['numeric_id'];
} else {
$connection = \OC::$server->getDatabaseConnection();
$available = $isAvailable ? 1 : 0;
if ($connection->insertIfNotExist('*PREFIX*storages', ['id' => $this->storageId, 'available' => $available])) {
- $this->numericId = $connection->lastInsertId('*PREFIX*storages');
+ $this->numericId = (int)$connection->lastInsertId('*PREFIX*storages');
} else {
if ($row = self::getStorageById($this->storageId)) {
- $this->numericId = $row['numeric_id'];
+ $this->numericId = (int)$row['numeric_id'];
} else {
throw new \RuntimeException('Storage could neither be inserted nor be selected from the database');
}
@@ -132,7 +132,7 @@ class Storage {
$storageId = self::adjustStorageId($storageId);
if ($row = self::getStorageById($storageId)) {
- return $row['numeric_id'];
+ return (int)$row['numeric_id'];
} else {
return null;
}
diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php
index f41468d4926..7d8c6d48b2c 100644
--- a/lib/private/User/Manager.php
+++ b/lib/private/User/Manager.php
@@ -157,6 +157,16 @@ class Manager extends PublicEmitter implements IUserManager {
return $this->cachedUsers[$uid];
}
+ if (method_exists($backend, 'loginName2UserName')) {
+ $loginName = $backend->loginName2UserName($uid);
+ if ($loginName !== false) {
+ $uid = $loginName;
+ }
+ if (isset($this->cachedUsers[$uid])) {
+ return $this->cachedUsers[$uid];
+ }
+ }
+
$user = new User($uid, $backend, $this, $this->config);
if ($cacheUser) {
$this->cachedUsers[$uid] = $user;
diff --git a/lib/private/legacy/image.php b/lib/private/legacy/image.php
index 2c20daf5d44..fee1a805c40 100644
--- a/lib/private/legacy/image.php
+++ b/lib/private/legacy/image.php
@@ -84,11 +84,6 @@ class OC_Image implements \OCP\IImage {
$this->logger = \OC::$server->getLogger();
}
- if (!extension_loaded('gd') || !function_exists('gd_info')) {
- $this->logger->error(__METHOD__ . '(): GD module not installed', array('app' => 'core'));
- return false;
- }
-
if (\OC_Util::fileInfoLoaded()) {
$this->fileInfo = new finfo(FILEINFO_MIME_TYPE);
}
@@ -802,8 +797,8 @@ class OC_Image implements \OCP\IImage {
$this->logger->error(__METHOD__ . '(): No image loaded', array('app' => 'core'));
return false;
}
- $widthOrig = imageSX($this->resource);
- $heightOrig = imageSY($this->resource);
+ $widthOrig = imagesx($this->resource);
+ $heightOrig = imagesy($this->resource);
$ratioOrig = $widthOrig / $heightOrig;
if ($ratioOrig > 1) {
@@ -828,8 +823,8 @@ class OC_Image implements \OCP\IImage {
$this->logger->error(__METHOD__ . '(): No image loaded', array('app' => 'core'));
return false;
}
- $widthOrig = imageSX($this->resource);
- $heightOrig = imageSY($this->resource);
+ $widthOrig = imagesx($this->resource);
+ $heightOrig = imagesy($this->resource);
$process = imagecreatetruecolor($width, $height);
if ($process == false) {
@@ -867,8 +862,8 @@ class OC_Image implements \OCP\IImage {
$this->logger->error('OC_Image->centerCrop, No image loaded', array('app' => 'core'));
return false;
}
- $widthOrig = imageSX($this->resource);
- $heightOrig = imageSY($this->resource);
+ $widthOrig = imagesx($this->resource);
+ $heightOrig = imagesy($this->resource);
if ($widthOrig === $heightOrig and $size == 0) {
return true;
}
@@ -967,8 +962,8 @@ class OC_Image implements \OCP\IImage {
$this->logger->error(__METHOD__ . '(): No image loaded', array('app' => 'core'));
return false;
}
- $widthOrig = imageSX($this->resource);
- $heightOrig = imageSY($this->resource);
+ $widthOrig = imagesx($this->resource);
+ $heightOrig = imagesy($this->resource);
$ratio = $widthOrig / $heightOrig;
$newWidth = min($maxWidth, $ratio * $maxHeight);
@@ -990,8 +985,8 @@ class OC_Image implements \OCP\IImage {
$this->logger->error(__METHOD__ . '(): No image loaded', array('app' => 'core'));
return false;
}
- $widthOrig = imageSX($this->resource);
- $heightOrig = imageSY($this->resource);
+ $widthOrig = imagesx($this->resource);
+ $heightOrig = imagesy($this->resource);
if ($widthOrig > $maxWidth || $heightOrig > $maxHeight) {
return $this->fitIn($maxWidth, $maxHeight);
@@ -1024,6 +1019,7 @@ if (!function_exists('imagebmp')) {
* @link http://www.programmierer-forum.de/imagebmp-gute-funktion-gefunden-t143716.htm
* @author mgutt <marc@gutt.it>
* @version 1.00
+ * @param resource $im
* @param string $fileName [optional] <p>The path to save the file to.</p>
* @param int $bit [optional] <p>Bit depth, (default is 24).</p>
* @param int $compression [optional]
diff --git a/settings/css/settings.css b/settings/css/settings.css
index d3fd395747e..6ed707f7c45 100644
--- a/settings/css/settings.css
+++ b/settings/css/settings.css
@@ -267,6 +267,15 @@ span.usersLastLoginTooltip { white-space: nowrap; }
top: 3px;
}
+#newuser .groups {
+ display: inline;
+}
+
+#newuser .groupsListContainer.hidden,
+#userlist .groupsListContainer.hidden {
+ display: none;
+}
+
tr:hover>td.password>span, tr:hover>td.displayName>span { margin:0; cursor:pointer; }
tr:hover>td.remove>a, tr:hover>td.password>img,tr:hover>td.displayName>img, tr:hover>td.quota>img { visibility:visible; cursor:pointer; }
td.remove {
diff --git a/settings/js/users/groups.js b/settings/js/users/groups.js
index e83f00970c2..8f4d95432a8 100644
--- a/settings/js/users/groups.js
+++ b/settings/js/users/groups.js
@@ -138,10 +138,6 @@ GroupList = {
var addedGroup = result.groupname;
UserList.availableGroups = $.unique($.merge(UserList.availableGroups, [addedGroup]));
GroupList.addGroup(result.groupname);
-
- $('.groupsselect, .subadminsselect')
- .append($('<option>', { value: result.groupname })
- .text(result.groupname));
}
GroupList.toggleAddGroup();
}).fail(function(result) {
diff --git a/settings/js/users/users.js b/settings/js/users/users.js
index 4ce77648826..f24bf82209b 100644
--- a/settings/js/users/users.js
+++ b/settings/js/users/users.js
@@ -59,9 +59,6 @@ var UserList = {
var $tr = $userListBody.find('tr:first-child').clone();
// this removes just the `display:none` of the template row
$tr.removeAttr('style');
- var subAdminsEl;
- var subAdminSelect;
- var groupsSelect;
/**
* Avatar or placeholder
@@ -88,32 +85,17 @@ var UserList = {
$tr.find('td.mailAddress > .action').tooltip({placement: 'top'});
$tr.find('td.password > .action').tooltip({placement: 'top'});
+
/**
* groups and subadmins
*/
- // make them look like the multiselect buttons
- // until they get time to really get initialized
- groupsSelect = $('<select multiple="multiple" class="groupsselect multiselect button" data-placehoder="Groups" title="' + t('settings', 'No group') + '"></select>')
- .data('username', user.name)
- .data('user-groups', user.groups);
- if ($tr.find('td.subadmins').length > 0) {
- subAdminSelect = $('<select multiple="multiple" class="subadminsselect multiselect button" data-placehoder="subadmins" title="' + t('settings', 'No group') + '">')
- .data('username', user.name)
- .data('user-groups', user.groups)
- .data('subadmin', user.subadmin);
- $tr.find('td.subadmins').empty();
- }
- $.each(this.availableGroups, function (i, group) {
- groupsSelect.append($('<option value="' + escapeHTML(group) + '">' + escapeHTML(group) + '</option>'));
- if (typeof subAdminSelect !== 'undefined' && group !== 'admin') {
- subAdminSelect.append($('<option value="' + escapeHTML(group) + '">' + escapeHTML(group) + '</option>'));
- }
- });
- $tr.find('td.groups').empty().append(groupsSelect);
- subAdminsEl = $tr.find('td.subadmins');
- if (subAdminsEl.length > 0) {
- subAdminsEl.append(subAdminSelect);
- }
+ var $tdGroups = $tr.find('td.groups');
+ this._updateGroupListLabel($tdGroups, user.groups);
+ $tdGroups.find('.action').tooltip({placement: 'top'});
+
+ var $tdSubadmins = $tr.find('td.subadmins');
+ this._updateGroupListLabel($tdSubadmins, user.subadmin);
+ $tdSubadmins.find('.action').tooltip({placement: 'top'});
/**
* remove action
@@ -200,10 +182,6 @@ var UserList = {
// defer init so the user first sees the list appear more quickly
window.setTimeout(function(){
$quotaSelect.singleSelect();
- UserList.applyGroupSelect(groupsSelect);
- if (subAdminSelect) {
- UserList.applySubadminSelect(subAdminSelect);
- }
}, 0);
return $tr;
},
@@ -324,7 +302,7 @@ var UserList = {
},
markRemove: function(uid) {
var $tr = UserList.getRow(uid);
- var groups = $tr.find('.groups .groupsselect').val();
+ var groups = $tr.find('.groups').data('groups');
for(var i in groups) {
var gid = groups[i];
var $li = GroupList.getGroupLI(gid);
@@ -339,7 +317,7 @@ var UserList = {
},
undoRemove: function(uid) {
var $tr = UserList.getRow(uid);
- var groups = $tr.find('.groups .groupsselect').val();
+ var groups = $tr.find('.groups').data('groups');
for(var i in groups) {
var gid = groups[i];
var $li = GroupList.getGroupLI(gid);
@@ -440,19 +418,9 @@ var UserList = {
});
},
- applyGroupSelect: function (element) {
- var checked = [];
+ applyGroupSelect: function (element, user, checked) {
var $element = $(element);
- var user = UserList.getUID($element);
- if ($element.data('user-groups')) {
- if (typeof $element.data('user-groups') === 'string') {
- checked = $element.data('user-groups').split(", ");
- }
- else {
- checked = $element.data('user-groups');
- }
- }
var checkHandler = null;
if(user) { // Only if in a user row, and not the #newusergroups select
checkHandler = function (group) {
@@ -492,13 +460,6 @@ var UserList = {
};
}
var addGroup = function (select, group) {
- $('select[multiple]').each(function (index, element) {
- $element = $(element);
- if ($element.find('option').filterAttr('value', group).length === 0 &&
- select.data('msid') !== $element.data('msid')) {
- $element.append('<option value="' + escapeHTML(group) + '">' + escapeHTML(group) + '</option>');
- }
- });
GroupList.addGroup(escapeHTML(group));
};
var label;
@@ -519,19 +480,8 @@ var UserList = {
});
},
- applySubadminSelect: function (element) {
- var checked = [];
+ applySubadminSelect: function (element, user, checked) {
var $element = $(element);
- var user = UserList.getUID($element);
-
- if ($element.data('subadmin')) {
- if (typeof $element.data('subadmin') === 'string') {
- checked = $element.data('subadmin').split(", ");
- }
- else {
- checked = $element.data('subadmin');
- }
- }
var checkHandler = function (group) {
if (group === 'admin') {
return false;
@@ -547,15 +497,7 @@ var UserList = {
);
};
- var addSubAdmin = function (group) {
- $('select[multiple]').each(function (index, element) {
- if ($(element).find('option').filterAttr('value', group).length === 0) {
- $(element).append('<option value="' + escapeHTML(group) + '">' + escapeHTML(group) + '</option>');
- }
- });
- };
$element.multiSelect({
- createCallback: addSubAdmin,
createText: null,
checked: checked,
oncheck: checkHandler,
@@ -613,6 +555,76 @@ var UserList = {
}
}
);
+ },
+
+ /**
+ * Creates a temporary jquery.multiselect selector on the given group field
+ */
+ _triggerGroupEdit: function($td, isSubadminSelect) {
+ var $groupsListContainer = $td.find('.groupsListContainer');
+ var placeholder = $groupsListContainer.attr('data-placeholder') || t('settings', 'no group');
+ var user = UserList.getUID($td);
+ var checked = $td.data('groups') || [];
+ var extraGroups = [].concat(checked);
+
+ $td.find('.multiselectoptions').remove();
+
+ // jquery.multiselect can only work with select+options in DOM ? We'll give jquery.multiselect what it wants...
+ var $groupsSelect;
+ if (isSubadminSelect) {
+ $groupsSelect = $('<select multiple="multiple" class="groupsselect multiselect button" title="' + placeholder + '"></select>');
+ } else {
+ $groupsSelect = $('<select multiple="multiple" class="subadminsselect multiselect button" title="' + placeholder + '"></select>')
+ }
+
+ function createItem(group) {
+ if (isSubadminSelect && group === 'admin') {
+ // can't become subadmin of "admin" group
+ return;
+ }
+ $groupsSelect.append($('<option value="' + escapeHTML(group) + '">' + escapeHTML(group) + '</option>'));
+ }
+
+ $.each(this.availableGroups, function (i, group) {
+ // some new groups might be selected but not in the available groups list yet
+ var extraIndex = extraGroups.indexOf(group);
+ if (extraIndex >= 0) {
+ // remove extra group as it was found
+ extraGroups.splice(extraIndex, 1);
+ }
+ createItem(group);
+ });
+ $.each(extraGroups, function (i, group) {
+ createItem(group);
+ });
+
+ $td.append($groupsSelect);
+
+ if (isSubadminSelect) {
+ UserList.applySubadminSelect($groupsSelect, user, checked);
+ } else {
+ UserList.applyGroupSelect($groupsSelect, user, checked);
+ }
+
+ $groupsListContainer.addClass('hidden');
+ $td.find('.multiselect:not(.groupsListContainer):first').click();
+ $groupsSelect.on('dropdownclosed', function(e) {
+ $groupsSelect.remove();
+ $td.find('.multiselect:not(.groupsListContainer)').parent().remove();
+ $td.find('.multiselectoptions').remove();
+ $groupsListContainer.removeClass('hidden');
+ UserList._updateGroupListLabel($td, e.checked);
+ });
+ },
+
+ /**
+ * Updates the groups list td with the given groups selection
+ */
+ _updateGroupListLabel: function($td, groups) {
+ var placeholder = $td.find('.groupsListContainer').attr('data-placeholder');
+ var $groupsEl = $td.find('.groupsList');
+ $groupsEl.text(groups.join(', ') || placeholder || t('settings', 'no group'));
+ $td.data('groups', groups);
}
};
@@ -637,13 +649,6 @@ $(document).ready(function () {
// TODO: move other init calls inside of initialize
UserList.initialize($('#userlist'));
- $('.groupsselect').each(function (index, element) {
- UserList.applyGroupSelect(element);
- });
- $('.subadminsselect').each(function (index, element) {
- UserList.applySubadminSelect(element);
- });
-
$userListBody.on('click', '.password', function (event) {
event.stopPropagation();
@@ -787,11 +792,24 @@ $(document).ready(function () {
});
});
+ $('#newuser .groupsListContainer').on('click', function (event) {
+ event.stopPropagation();
+ var $div = $(this).closest('.groups');
+ UserList._triggerGroupEdit($div);
+ });
+ $userListBody.on('click', '.groups .groupsListContainer, .subadmins .groupsListContainer', function (event) {
+ event.stopPropagation();
+ var $td = $(this).closest('td');
+ var isSubadminSelect = $td.hasClass('subadmins');
+ UserList._triggerGroupEdit($td, isSubadminSelect);
+ });
+
// init the quota field select box after it is shown the first time
$('#app-settings').one('show', function() {
$(this).find('#default_quota').singleSelect().on('change', UserList.onQuotaSelect);
});
+ UserList._updateGroupListLabel($('#newuser .groups'), []);
$('#newuser').submit(function (event) {
event.preventDefault();
var username = $('#newusername').val();
@@ -827,7 +845,7 @@ $(document).ready(function () {
}
promise.then(function() {
- var groups = $('#newusergroups').val() || [];
+ var groups = $('#newuser .groups').data('groups') || [];
$.post(
OC.generateUrl('/settings/users/users'),
{
diff --git a/settings/templates/users/main.php b/settings/templates/users/main.php
index f50f83b38b3..b363a4c4da8 100644
--- a/settings/templates/users/main.php
+++ b/settings/templates/users/main.php
@@ -19,10 +19,10 @@ style('settings', 'settings');
$userlistParams = array();
$allGroups=array();
-foreach($_["groups"] as $group) {
+foreach($_["adminGroup"] as $group) {
$allGroups[] = $group['name'];
}
-foreach($_["adminGroup"] as $group) {
+foreach($_["groups"] as $group) {
$allGroups[] = $group['name'];
}
$userlistParams['subadmingroups'] = $allGroups;
diff --git a/settings/templates/users/part.createuser.php b/settings/templates/users/part.createuser.php
index 0fc5a2bdeaa..6f23d06cfa3 100644
--- a/settings/templates/users/part.createuser.php
+++ b/settings/templates/users/part.createuser.php
@@ -10,16 +10,7 @@
<input id="newemail" type="text" style="display:none"
placeholder="<?php p($l->t('E-Mail'))?>"
autocomplete="off" autocapitalize="off" autocorrect="off" />
- <select
- class="groupsselect" id="newusergroups" data-placeholder="groups"
- title="<?php p($l->t('Groups'))?>" multiple="multiple">
- <?php foreach($_["adminGroup"] as $adminGroup): ?>
- <option value="<?php p($adminGroup['name']);?>"><?php p($adminGroup['name']); ?></option>
- <?php endforeach; ?>
- <?php foreach($_["groups"] as $group): ?>
- <option value="<?php p($group['name']);?>"><?php p($group['name']);?></option>
- <?php endforeach;?>
- </select>
+ <div class="groups"><div class="groupsListContainer multiselect button" data-placeholder="<?php p($l->t('Groups'))?>"><span class="title groupsList"></span><span class="icon-triangle-s"></span></div></div>
<input type="submit" class="button" value="<?php p($l->t('Create'))?>" />
</form>
<?php if((bool)$_['recoveryAdminEnabled']): ?>
diff --git a/settings/templates/users/part.userlist.php b/settings/templates/users/part.userlist.php
index 2bdd0714a3c..bab68e5a765 100644
--- a/settings/templates/users/part.userlist.php
+++ b/settings/templates/users/part.userlist.php
@@ -38,9 +38,13 @@
src="<?php p(image_path('core', 'actions/rename.svg'))?>"
alt="<?php p($l->t('change email address'))?>" title="<?php p($l->t('change email address'))?>"/>
</td>
- <td class="groups"></td>
+ <td class="groups"><div class="groupsListContainer multiselect button"
+ ><span class="title groupsList"></span><span class="icon-triangle-s"></span></div>
+ </td>
<?php if(is_array($_['subadmins']) || $_['subadmins']): ?>
- <td class="subadmins"></td>
+ <td class="subadmins"><div class="groupsListContainer multiselect button"
+ ><span class="title groupsList"></span><span class="icon-triangle-s"></span></div>
+ </td>
<?php endif;?>
<td class="quota">
<select class="quota-user" data-inputtitle="<?php p($l->t('Please enter storage quota (ex: "512 MB" or "12 GB")')) ?>">
diff --git a/tests/Core/Controller/LoginControllerTest.php b/tests/Core/Controller/LoginControllerTest.php
index 417a60a9e5f..ff50ac98fbd 100644
--- a/tests/Core/Controller/LoginControllerTest.php
+++ b/tests/Core/Controller/LoginControllerTest.php
@@ -505,7 +505,7 @@ class LoginControllerTest extends TestCase {
$this->assertEquals($expected, $this->loginController->tryLogin('Jane', $password, $originalUrl));
}
- public function testLoginWithTwoFactorEnforced() {
+ public function testLoginWithOneTwoFactorProvider() {
/** @var IUser | \PHPUnit_Framework_MockObject_MockObject $user */
$user = $this->getMockBuilder('\OCP\IUser')->getMock();
$user->expects($this->any())
@@ -513,6 +513,7 @@ class LoginControllerTest extends TestCase {
->will($this->returnValue('john'));
$password = 'secret';
$challengeUrl = 'challenge/url';
+ $provider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock();
$this->request
->expects($this->exactly(2))
@@ -547,6 +548,79 @@ class LoginControllerTest extends TestCase {
$this->twoFactorManager->expects($this->once())
->method('prepareTwoFactorLogin')
->with($user);
+ $this->twoFactorManager->expects($this->once())
+ ->method('getProviders')
+ ->with($user)
+ ->will($this->returnValue([$provider]));
+ $provider->expects($this->once())
+ ->method('getId')
+ ->will($this->returnValue('u2f'));
+ $this->urlGenerator->expects($this->once())
+ ->method('linkToRoute')
+ ->with('core.TwoFactorChallenge.showChallenge', [
+ 'challengeProviderId' => 'u2f',
+ ])
+ ->will($this->returnValue($challengeUrl));
+ $this->config->expects($this->once())
+ ->method('deleteUserValue')
+ ->with('john', 'core', 'lostpassword');
+
+ $expected = new RedirectResponse($challengeUrl);
+ $this->assertEquals($expected, $this->loginController->tryLogin('john@doe.com', $password, null));
+ }
+
+ public function testLoginWithMultpleTwoFactorProviders() {
+ /** @var IUser | \PHPUnit_Framework_MockObject_MockObject $user */
+ $user = $this->getMockBuilder('\OCP\IUser')->getMock();
+ $user->expects($this->any())
+ ->method('getUID')
+ ->will($this->returnValue('john'));
+ $password = 'secret';
+ $challengeUrl = 'challenge/url';
+ $provider1 = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock();
+ $provider2 = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock();
+
+ $this->request
+ ->expects($this->exactly(2))
+ ->method('getRemoteAddress')
+ ->willReturn('192.168.0.1');
+ $this->request
+ ->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->willReturn(true);
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelay')
+ ->with('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('getDelay')
+ ->with('192.168.0.1')
+ ->willReturn(200);
+ $this->userManager->expects($this->once())
+ ->method('checkPassword')
+ ->will($this->returnValue($user));
+ $this->userSession->expects($this->once())
+ ->method('login')
+ ->with('john@doe.com', $password);
+ $this->userSession->expects($this->once())
+ ->method('createSessionToken')
+ ->with($this->request, $user->getUID(), 'john@doe.com', $password);
+ $this->twoFactorManager->expects($this->once())
+ ->method('isTwoFactorAuthenticated')
+ ->with($user)
+ ->will($this->returnValue(true));
+ $this->twoFactorManager->expects($this->once())
+ ->method('prepareTwoFactorLogin')
+ ->with($user);
+ $this->twoFactorManager->expects($this->once())
+ ->method('getProviders')
+ ->with($user)
+ ->will($this->returnValue([$provider1, $provider2]));
+ $provider1->expects($this->never())
+ ->method('getId');
+ $provider2->expects($this->never())
+ ->method('getId');
$this->urlGenerator->expects($this->once())
->method('linkToRoute')
->with('core.TwoFactorChallenge.selectChallenge')
diff --git a/tests/lib/Repair/RepairLegacyStoragesTest.php b/tests/lib/Repair/RepairLegacyStoragesTest.php
index aa51fe06a35..8d8366dde06 100644
--- a/tests/lib/Repair/RepairLegacyStoragesTest.php
+++ b/tests/lib/Repair/RepairLegacyStoragesTest.php
@@ -98,23 +98,9 @@ class RepairLegacyStoragesTest extends TestCase {
$storageId = Storage::adjustStorageId($storageId);
$numRows = $this->connection->executeUpdate($sql, array($storageId));
- $this->assertEquals(1, $numRows);
+ $this->assertSame(1, $numRows);
- return \OC::$server->getDatabaseConnection()->lastInsertId('*PREFIX*storages');
- }
-
- /**
- * Returns the storage id based on the numeric id
- *
- * @param int $storageId numeric id of the storage
- * @return string storage id or null if not found
- */
- private function getStorageId($storageId) {
- $numericId = Storage::getNumericStorageId($storageId);
- if (!is_null($numericId)) {
- return (int)$numericId;
- }
- return null;
+ return (int)\OC::$server->getDatabaseConnection()->lastInsertId('*PREFIX*storages');
}
/**
@@ -144,8 +130,8 @@ class RepairLegacyStoragesTest extends TestCase {
$this->repair->run($this->outputMock);
- $this->assertNull($this->getStorageId($this->legacyStorageId));
- $this->assertEquals($newStorageNumId, $this->getStorageId($this->newStorageId));
+ $this->assertNull(Storage::getNumericStorageId($this->legacyStorageId));
+ $this->assertSame($newStorageNumId, Storage::getNumericStorageId($this->newStorageId));
}
/**
@@ -163,8 +149,8 @@ class RepairLegacyStoragesTest extends TestCase {
$this->repair->run($this->outputMock);
- $this->assertNull($this->getStorageId($this->legacyStorageId));
- $this->assertEquals($legacyStorageNumId, $this->getStorageId($this->newStorageId));
+ $this->assertNull(Storage::getNumericStorageId($this->legacyStorageId));
+ $this->assertSame($legacyStorageNumId, Storage::getNumericStorageId($this->newStorageId));
}
/**
@@ -185,8 +171,8 @@ class RepairLegacyStoragesTest extends TestCase {
$this->repair->run($this->outputMock);
- $this->assertNull($this->getStorageId($this->legacyStorageId));
- $this->assertEquals($legacyStorageNumId, $this->getStorageId($this->newStorageId));
+ $this->assertNull(Storage::getNumericStorageId($this->legacyStorageId));
+ $this->assertSame($legacyStorageNumId, Storage::getNumericStorageId($this->newStorageId));
}
/**
@@ -208,8 +194,8 @@ class RepairLegacyStoragesTest extends TestCase {
$this->repair->run($this->outputMock);
- $this->assertNull($this->getStorageId($this->legacyStorageId));
- $this->assertEquals($newStorageNumId, $this->getStorageId($this->newStorageId));
+ $this->assertNull(Storage::getNumericStorageId($this->legacyStorageId));
+ $this->assertSame($newStorageNumId, Storage::getNumericStorageId($this->newStorageId));
}
/**
@@ -233,8 +219,8 @@ class RepairLegacyStoragesTest extends TestCase {
$this->repair->run($this->outputMock);
// storages left alone
- $this->assertEquals($legacyStorageNumId, $this->getStorageId($this->legacyStorageId));
- $this->assertEquals($newStorageNumId, $this->getStorageId($this->newStorageId));
+ $this->assertSame($legacyStorageNumId, Storage::getNumericStorageId($this->legacyStorageId));
+ $this->assertSame($newStorageNumId, Storage::getNumericStorageId($this->newStorageId));
// do not set the done flag
$this->assertNotEquals('yes', $this->config->getAppValue('core', 'repairlegacystoragesdone'));
@@ -255,7 +241,7 @@ class RepairLegacyStoragesTest extends TestCase {
$this->repair->run($this->outputMock);
- $this->assertEquals($numId, $this->getStorageId($storageId));
+ $this->assertSame($numId, Storage::getNumericStorageId($storageId));
}
/**
@@ -273,7 +259,7 @@ class RepairLegacyStoragesTest extends TestCase {
$this->repair->run($this->outputMock);
- $this->assertEquals($numId, $this->getStorageId($storageId));
+ $this->assertSame($numId, Storage::getNumericStorageId($storageId));
}
/**
@@ -291,7 +277,7 @@ class RepairLegacyStoragesTest extends TestCase {
$this->repair->run($this->outputMock);
- $this->assertEquals($numId, $this->getStorageId($storageId));
+ $this->assertSame($numId, Storage::getNumericStorageId($storageId));
}
/**