diff options
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)); } /** |