diff options
87 files changed, 1066 insertions, 337 deletions
diff --git a/3rdparty b/3rdparty -Subproject f4e328bc4cc67011d206ca024483531a3b8c544 +Subproject 696f7683651fa2ee1f59cbde08c6f5fefbcaad0 diff --git a/apps/comments/js/commentstabview.js b/apps/comments/js/commentstabview.js index 9b75cb4671e..bd89c8bbb49 100644 --- a/apps/comments/js/commentstabview.js +++ b/apps/comments/js/commentstabview.js @@ -512,7 +512,7 @@ _onTypeComment: function(ev) { var $field = $(ev.target); - var len = $field.val().length; + var len = $field.text().length; var $submitButton = $field.data('submitButtonEl'); if (!$submitButton) { $submitButton = $field.closest('form').find('.submit'); diff --git a/apps/comments/tests/js/commentstabviewSpec.js b/apps/comments/tests/js/commentstabviewSpec.js index 813b2a72eae..0131bc7bce3 100644 --- a/apps/comments/tests/js/commentstabviewSpec.js +++ b/apps/comments/tests/js/commentstabviewSpec.js @@ -411,7 +411,7 @@ describe('OCA.Comments.CommentsTabView tests', function() { expect($message.hasClass('error')).toEqual(false); }); it('displays tooltip when limit is almost reached', function() { - $message.val(createMessageWithLength(view._commentMaxLength - 2)); + $message.text(createMessageWithLength(view._commentMaxLength - 2)); $message.trigger('change'); expect(tooltipStub.calledWith('show')).toEqual(true); @@ -419,7 +419,7 @@ describe('OCA.Comments.CommentsTabView tests', function() { expect($message.hasClass('error')).toEqual(false); }); it('displays tooltip and disabled button when limit is exceeded', function() { - $message.val(createMessageWithLength(view._commentMaxLength + 2)); + $message.text(createMessageWithLength(view._commentMaxLength + 2)); $message.trigger('change'); expect(tooltipStub.calledWith('show')).toEqual(true); diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index 0f97289ba37..25a0d542e85 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -5,7 +5,7 @@ <description>WebDAV endpoint</description> <licence>AGPL</licence> <author>owncloud.org</author> - <version>1.4.5</version> + <version>1.4.6</version> <default_enable/> <types> <filesystem/> @@ -17,9 +17,6 @@ <dependencies> <nextcloud min-version="13" max-version="13" /> </dependencies> - <background-jobs> - <job>OCA\DAV\CardDAV\SyncJob</job> - </background-jobs> <repair-steps> <post-migration> <step>OCA\DAV\Migration\FixBirthdayCalendarComponent</step> diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index ddb8d7ca8d2..4d2db8e2f5e 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -59,7 +59,6 @@ return array( 'OCA\\DAV\\CardDAV\\ImageExportPlugin' => $baseDir . '/../lib/CardDAV/ImageExportPlugin.php', 'OCA\\DAV\\CardDAV\\PhotoCache' => $baseDir . '/../lib/CardDAV/PhotoCache.php', 'OCA\\DAV\\CardDAV\\Plugin' => $baseDir . '/../lib/CardDAV/Plugin.php', - 'OCA\\DAV\\CardDAV\\SyncJob' => $baseDir . '/../lib/CardDAV/SyncJob.php', 'OCA\\DAV\\CardDAV\\SyncService' => $baseDir . '/../lib/CardDAV/SyncService.php', 'OCA\\DAV\\CardDAV\\UserAddressBooks' => $baseDir . '/../lib/CardDAV/UserAddressBooks.php', 'OCA\\DAV\\CardDAV\\Xml\\Groups' => $baseDir . '/../lib/CardDAV/Xml/Groups.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 46707a93912..42f2e6da286 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -74,7 +74,6 @@ class ComposerStaticInitDAV 'OCA\\DAV\\CardDAV\\ImageExportPlugin' => __DIR__ . '/..' . '/../lib/CardDAV/ImageExportPlugin.php', 'OCA\\DAV\\CardDAV\\PhotoCache' => __DIR__ . '/..' . '/../lib/CardDAV/PhotoCache.php', 'OCA\\DAV\\CardDAV\\Plugin' => __DIR__ . '/..' . '/../lib/CardDAV/Plugin.php', - 'OCA\\DAV\\CardDAV\\SyncJob' => __DIR__ . '/..' . '/../lib/CardDAV/SyncJob.php', 'OCA\\DAV\\CardDAV\\SyncService' => __DIR__ . '/..' . '/../lib/CardDAV/SyncService.php', 'OCA\\DAV\\CardDAV\\UserAddressBooks' => __DIR__ . '/..' . '/../lib/CardDAV/UserAddressBooks.php', 'OCA\\DAV\\CardDAV\\Xml\\Groups' => __DIR__ . '/..' . '/../lib/CardDAV/Xml/Groups.php', diff --git a/apps/dav/lib/CardDAV/SyncJob.php b/apps/dav/lib/CardDAV/SyncJob.php deleted file mode 100644 index f0f8d51c2ca..00000000000 --- a/apps/dav/lib/CardDAV/SyncJob.php +++ /dev/null @@ -1,42 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\CardDAV; - -use OC\BackgroundJob\TimedJob; -use OCA\DAV\AppInfo\Application; - -class SyncJob extends TimedJob { - - public function __construct() { - // Run once a day - $this->setInterval(24 * 60 * 60); - } - - protected function run($argument) { - $app = new Application(); - /** @var SyncService $ss */ - $ss = $app->getSyncService(); - $ss->syncInstance(); - } -} diff --git a/apps/files/css/files.scss b/apps/files/css/files.scss index 1be3c9216f0..00060ee7bd6 100644 --- a/apps/files/css/files.scss +++ b/apps/files/css/files.scss @@ -20,7 +20,11 @@ .actions.creatable { position: relative; - z-index: -30; + display: flex; + flex: 1 1; + .button:not(:last-child) { + margin-right: 3px; + } } #trash { @@ -43,10 +47,6 @@ width: 100%; } -#filestable.has-controls { - top: 44px; -} - #filestable tbody tr { height: 51px; } @@ -242,12 +242,12 @@ table th.column-last, table td.column-last { /* Multiselect bar */ #filestable.multiselect { - top: 95px; + top: 51px; } table.multiselect thead { position: fixed; top: 89px; - z-index: 10; + z-index: 55; -moz-box-sizing: border-box; box-sizing: border-box; left: 250px; /* sidebar */ @@ -649,6 +649,7 @@ table tr.summary td { table.dragshadow { width:auto; + z-index: 100; } table.dragshadow td.filename { padding-left:60px; diff --git a/apps/files/css/mobile.scss b/apps/files/css/mobile.scss index 12c9e4fa2d3..10fa29e7a38 100644 --- a/apps/files/css/mobile.scss +++ b/apps/files/css/mobile.scss @@ -69,4 +69,9 @@ table td.filename .nametext .innernametext { display: block !important; } +/* ensure that it is visible over #app-content */ +table.dragshadow { + z-index: 1000; +} + } diff --git a/apps/files/js/breadcrumb.js b/apps/files/js/breadcrumb.js index 35aeb8d357d..20b15e3cb93 100644 --- a/apps/files/js/breadcrumb.js +++ b/apps/files/js/breadcrumb.js @@ -331,7 +331,7 @@ // Used for testing since this.$el.parent fails if (!this.availableWidth) { - this.usedWidth = this.$el.parent().width() - (this.$el.parent().find('.button').length + 1) * 44; + this.usedWidth = this.$el.parent().width() - this.$el.parent().find('.actions.creatable').width(); } else { this.usedWidth = this.availableWidth; } diff --git a/apps/files/js/file-upload.js b/apps/files/js/file-upload.js index d1730fa7bc7..e9534111e10 100644 --- a/apps/files/js/file-upload.js +++ b/apps/files/js/file-upload.js @@ -130,7 +130,7 @@ OC.FileUpload.prototype = { }, /** - * Get full path for the target file, + * Get full path for the target file, * including relative path and file name. * * @return {String} full path diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js index 6c031ab06d5..2fb7dfba29f 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -47,14 +47,6 @@ */ $el: null, - /** - * List of handlers to be notified whenever a register() or - * setDefault() was called. - * - * @member {Function[]} - */ - _updateListeners: {}, - _fileActionTriggerTemplate: null, /** @@ -142,7 +134,22 @@ var mime = action.mime; var name = action.name; var actionSpec = { - action: action.actionHandler, + action: function(fileName, context) { + // Actions registered in one FileAction may be executed on a + // different one (for example, due to the "merge" function), + // so the listeners have to be updated on the FileActions + // from the context instead of on the one in which it was + // originally registered. + if (context && context.fileActions) { + context.fileActions._notifyUpdateListeners('beforeTriggerAction', {action: actionSpec, fileName: fileName, context: context}); + } + + action.actionHandler(fileName, context); + + if (context && context.fileActions) { + context.fileActions._notifyUpdateListeners('afterTriggerAction', {action: actionSpec, fileName: fileName, context: context}); + } + }, name: name, displayName: action.displayName, mime: mime, @@ -174,7 +181,6 @@ this.defaults = {}; this.icons = {}; this.currentFile = null; - this._updateListeners = []; }, /** * Sets the default action for a given mime type. diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index fa9819b78b5..d0c0fc1a7fc 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -676,8 +676,28 @@ $(event.target).closest('a').blur(); } } else { - this._updateDetailsView($tr.attr('data-file')); + // Even if there is no Details action the default event + // handler is prevented for consistency (although there + // should always be a Details action); otherwise the link + // would be downloaded by the browser when the user expected + // the details to be shown. event.preventDefault(); + var filename = $tr.attr('data-file'); + this.fileActions.currentFile = $tr.find('td'); + var mime = this.fileActions.getCurrentMimeType(); + var type = this.fileActions.getCurrentType(); + var permissions = this.fileActions.getCurrentPermissions(); + var action = this.fileActions.get(mime, type, permissions)['Details']; + if (action) { + // also set on global object for legacy apps + window.FileActions.currentFile = this.fileActions.currentFile; + action(filename, { + $file: $tr, + fileList: this, + fileActions: this.fileActions, + dir: $tr.attr('data-path') || this.getCurrentDirectory() + }); + } } } }, @@ -1751,7 +1771,6 @@ return true; } - // TODO: parse remaining quota from PROPFIND response this.updateStorageStatistics(true); // first entry is the root diff --git a/apps/files/js/files.js b/apps/files/js/files.js index 017bf7ecf41..153307fec52 100644 --- a/apps/files/js/files.js +++ b/apps/files/js/files.js @@ -29,6 +29,7 @@ state.dir = null; state.call = null; Files.updateMaxUploadFilesize(response); + Files.updateQuota(response); }); }, /** @@ -77,6 +78,32 @@ }, + updateQuota:function(response) { + if (response === undefined) { + return; + } + if (response.data !== undefined + && response.data.quota !== undefined + && response.data.used !== undefined + && response.data.usedSpacePercent !== undefined) { + var humanUsed = OC.Util.humanFileSize(response.data.used, true); + var humanQuota = OC.Util.humanFileSize(response.data.quota, true); + if (response.data.quota > 0) { + $('#quota').attr('data-original-title', Math.floor(response.data.used/response.data.quota*1000)/10 + '%'); + $('#quota progress').val(response.data.usedSpacePercent); + $('#quotatext').text(t('files', '{used} of {quota} used', {used: humanUsed, quota: humanQuota})); + } else { + $('#quotatext').text(t('files', '{used} used', {used: humanUsed})); + } + if (response.data.usedSpacePercent > 80) { + $('#quota progress').addClass('warn'); + } else { + $('#quota progress').removeClass('warn'); + } + } + + }, + /** * Fix path name by removing double slash at the beginning, if any */ @@ -383,7 +410,6 @@ var dragOptions={ revert: 'invalid', revertDuration: 300, opacity: 0.7, - zIndex: 100, appendTo: 'body', cursorAt: { left: 24, top: 18 }, helper: createDragShadow, diff --git a/apps/files/lib/Helper.php b/apps/files/lib/Helper.php index ab952c97dfb..9d9717c9401 100644 --- a/apps/files/lib/Helper.php +++ b/apps/files/lib/Helper.php @@ -56,6 +56,8 @@ class Helper { 'uploadMaxFilesize' => $maxUploadFileSize, 'maxHumanFilesize' => $maxHumanFileSize, 'freeSpace' => $storageInfo['free'], + 'quota' => $storageInfo['quota'], + 'used' => $storageInfo['used'], 'usedSpacePercent' => (int)$storageInfo['relative'], 'owner' => $storageInfo['owner'], 'ownerDisplayName' => $storageInfo['ownerDisplayName'], diff --git a/apps/files/templates/appnavigation.php b/apps/files/templates/appnavigation.php index 955cd03a019..5d270914ff1 100644 --- a/apps/files/templates/appnavigation.php +++ b/apps/files/templates/appnavigation.php @@ -11,7 +11,7 @@ </a> </li> <?php } ?> - <li id="quota" class="pinned <?php + <li id="quota" class="pinned <?php p($pinned===0?'first-pinned ':'') ?><?php if ($_['quota'] !== \OCP\Files\FileInfo::SPACE_UNLIMITED) { ?>has-tooltip" title="<?php p($_['usage_relative'] . '%'); } ?>"> diff --git a/apps/files/tests/js/appSpec.js b/apps/files/tests/js/appSpec.js index b9c323e7c12..5728991e197 100644 --- a/apps/files/tests/js/appSpec.js +++ b/apps/files/tests/js/appSpec.js @@ -112,9 +112,22 @@ describe('OCA.Files.App tests', function() { App.initialize(); var actions = App.fileList.fileActions.actions; - expect(actions.all.OverwriteThis.action).toBe(actionStub); - expect(actions.all.LegacyTest.action).toBe(legacyActionStub); - expect(actions.all.RegularTest.action).toBe(actionStub); + var context = { fileActions: sinon.createStubInstance(OCA.Files.FileActions) }; + actions.all.OverwriteThis.action('testFileName', context); + expect(actionStub.calledOnce).toBe(true); + expect(context.fileActions._notifyUpdateListeners.callCount).toBe(2); + expect(context.fileActions._notifyUpdateListeners.getCall(0).calledWith('beforeTriggerAction')).toBe(true); + expect(context.fileActions._notifyUpdateListeners.getCall(1).calledWith('afterTriggerAction')).toBe(true); + actions.all.LegacyTest.action('testFileName', context); + expect(legacyActionStub.calledOnce).toBe(true); + expect(context.fileActions._notifyUpdateListeners.callCount).toBe(4); + expect(context.fileActions._notifyUpdateListeners.getCall(2).calledWith('beforeTriggerAction')).toBe(true); + expect(context.fileActions._notifyUpdateListeners.getCall(3).calledWith('afterTriggerAction')).toBe(true); + actions.all.RegularTest.action('testFileName', context); + expect(actionStub.calledTwice).toBe(true); + expect(context.fileActions._notifyUpdateListeners.callCount).toBe(6); + expect(context.fileActions._notifyUpdateListeners.getCall(4).calledWith('beforeTriggerAction')).toBe(true); + expect(context.fileActions._notifyUpdateListeners.getCall(5).calledWith('afterTriggerAction')).toBe(true); // default one still there expect(actions.dir.Open.action).toBeDefined(); }); diff --git a/apps/files/tests/js/breadcrumbSpec.js b/apps/files/tests/js/breadcrumbSpec.js index dd3eac017ec..5ec5ad2d6e8 100644 --- a/apps/files/tests/js/breadcrumbSpec.js +++ b/apps/files/tests/js/breadcrumbSpec.js @@ -242,6 +242,17 @@ describe('OCA.Files.BreadCrumb tests', function() { dummyDir = '/short name/longer name/looooooooooooonger/' + 'even longer long long long longer long/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/last one'; + bc = new BreadCrumb(); + // append dummy navigation and controls + // as they are currently used for measurements + $('#testArea').append( + '<div id="controls"></div>' + ); + $('#controls').append(bc.$el); + + // triggers resize implicitly + bc.setDirectory(dummyDir); + // using hard-coded widths (pre-measured) to avoid getting different // results on different browsers due to font engine differences // 51px is default size for menu and home @@ -250,14 +261,6 @@ describe('OCA.Files.BreadCrumb tests', function() { $('div.crumb').each(function(index){ $(this).css('width', widths[index]); }); - - bc = new BreadCrumb(); - // append dummy navigation and controls - // as they are currently used for measurements - $('#testArea').append( - '<div id="controls"></div>' - ); - $('#controls').append(bc.$el); }); afterEach(function() { bc = null; @@ -267,8 +270,6 @@ describe('OCA.Files.BreadCrumb tests', function() { bc.setMaxWidth(500); - // triggers resize implicitly - bc.setDirectory(dummyDir); $crumbs = bc.$el.find('.crumb'); // Menu and home are always visible @@ -282,6 +283,24 @@ describe('OCA.Files.BreadCrumb tests', function() { expect($crumbs.eq(6).hasClass('hidden')).toEqual(true); expect($crumbs.eq(7).hasClass('hidden')).toEqual(false); }); + it('Hides breadcrumbs to fit max allowed width', function() { + var $crumbs; + + bc.setMaxWidth(700); + + $crumbs = bc.$el.find('.crumb'); + + // Menu and home are always visible + expect($crumbs.eq(0).hasClass('hidden')).toEqual(false); + expect($crumbs.eq(1).hasClass('hidden')).toEqual(false); + + expect($crumbs.eq(2).hasClass('hidden')).toEqual(false); + expect($crumbs.eq(3).hasClass('hidden')).toEqual(false); + expect($crumbs.eq(4).hasClass('hidden')).toEqual(true); + expect($crumbs.eq(5).hasClass('hidden')).toEqual(true); + expect($crumbs.eq(6).hasClass('hidden')).toEqual(false); + expect($crumbs.eq(7).hasClass('hidden')).toEqual(false); + }); it('Updates the breadcrumbs when reducing max allowed width', function() { var $crumbs; @@ -290,7 +309,7 @@ describe('OCA.Files.BreadCrumb tests', function() { $crumbs = bc.$el.find('.crumb'); // Menu is hidden - expect($crumbs.eq(0).hasClass('hidden')).toEqual(false); + expect($crumbs.eq(0).hasClass('hidden')).toEqual(true); // triggers resize implicitly bc.setDirectory(dummyDir); @@ -304,7 +323,7 @@ describe('OCA.Files.BreadCrumb tests', function() { expect($crumbs.eq(2).hasClass('hidden')).toEqual(false); expect($crumbs.eq(3).hasClass('hidden')).toEqual(false); - expect($crumbs.eq(4).hasClass('hidden')).toEqual(false); + expect($crumbs.eq(4).hasClass('hidden')).toEqual(true); expect($crumbs.eq(5).hasClass('hidden')).toEqual(false); expect($crumbs.eq(6).hasClass('hidden')).toEqual(false); expect($crumbs.eq(7).hasClass('hidden')).toEqual(false); diff --git a/apps/files/tests/js/fileactionsSpec.js b/apps/files/tests/js/fileactionsSpec.js index 75a18713696..2dc8bb50920 100644 --- a/apps/files/tests/js/fileactionsSpec.js +++ b/apps/files/tests/js/fileactionsSpec.js @@ -299,6 +299,7 @@ describe('OCA.Files.FileActions tests', function() { clock.restore(); }); it('passes context to action handler', function() { + var notifyUpdateListenersSpy = sinon.spy(fileList.fileActions, '_notifyUpdateListeners'); $tr.find('.action-test').click(); expect(actionStub.calledOnce).toEqual(true); expect(actionStub.getCall(0).args[0]).toEqual('testName.txt'); @@ -309,6 +310,22 @@ describe('OCA.Files.FileActions tests', function() { expect(context.dir).toEqual('/subdir'); expect(context.fileInfoModel.get('name')).toEqual('testName.txt'); + expect(notifyUpdateListenersSpy.calledTwice).toEqual(true); + expect(notifyUpdateListenersSpy.calledBefore(actionStub)).toEqual(true); + expect(notifyUpdateListenersSpy.calledAfter(actionStub)).toEqual(true); + expect(notifyUpdateListenersSpy.getCall(0).args[0]).toEqual('beforeTriggerAction'); + expect(notifyUpdateListenersSpy.getCall(0).args[1]).toEqual({ + action: fileActions.getActions('all', OCA.Files.FileActions.TYPE_INLINE, OC.PERMISSION_READ)['Test'], + fileName: 'testName.txt', + context: context + }); + expect(notifyUpdateListenersSpy.getCall(1).args[0]).toEqual('afterTriggerAction'); + expect(notifyUpdateListenersSpy.getCall(1).args[1]).toEqual({ + action: fileActions.getActions('all', OCA.Files.FileActions.TYPE_INLINE, OC.PERMISSION_READ)['Test'], + fileName: 'testName.txt', + context: context + }); + // when data-path is defined actionStub.reset(); $tr.attr('data-path', '/somepath'); @@ -317,6 +334,7 @@ describe('OCA.Files.FileActions tests', function() { expect(context.dir).toEqual('/somepath'); }); it('also triggers action handler when calling triggerAction()', function() { + var notifyUpdateListenersSpy = sinon.spy(fileList.fileActions, '_notifyUpdateListeners'); var model = new OCA.Files.FileInfoModel({ id: 1, name: 'Test.txt', @@ -331,7 +349,62 @@ describe('OCA.Files.FileActions tests', function() { expect(actionStub.getCall(0).args[1].fileList).toEqual(fileList); expect(actionStub.getCall(0).args[1].fileActions).toEqual(fileActions); expect(actionStub.getCall(0).args[1].fileInfoModel).toEqual(model); + + expect(notifyUpdateListenersSpy.calledTwice).toEqual(true); + expect(notifyUpdateListenersSpy.calledBefore(actionStub)).toEqual(true); + expect(notifyUpdateListenersSpy.calledAfter(actionStub)).toEqual(true); + expect(notifyUpdateListenersSpy.getCall(0).args[0]).toEqual('beforeTriggerAction'); + expect(notifyUpdateListenersSpy.getCall(0).args[1]).toEqual({ + action: fileActions.getActions('all', OCA.Files.FileActions.TYPE_INLINE, OC.PERMISSION_READ)['Test'], + fileName: 'Test.txt', + context: { + fileActions: fileActions, + fileInfoModel: model, + dir: '/subdir', + fileList: fileList, + $file: fileList.findFileEl('Test.txt') + } + }); + expect(notifyUpdateListenersSpy.getCall(1).args[0]).toEqual('afterTriggerAction'); + expect(notifyUpdateListenersSpy.getCall(1).args[1]).toEqual({ + action: fileActions.getActions('all', OCA.Files.FileActions.TYPE_INLINE, OC.PERMISSION_READ)['Test'], + fileName: 'Test.txt', + context: { + fileActions: fileActions, + fileInfoModel: model, + dir: '/subdir', + fileList: fileList, + $file: fileList.findFileEl('Test.txt') + } + }); }); + it('triggers listener events when invoked directly', function() { + var context = {fileActions: new OCA.Files.FileActions()} + var notifyUpdateListenersSpy = sinon.spy(context.fileActions, '_notifyUpdateListeners'); + var testAction = fileActions.get('all', OCA.Files.FileActions.TYPE_INLINE, OC.PERMISSION_READ)['Test']; + + testAction('Test.txt', context); + + expect(actionStub.calledOnce).toEqual(true); + expect(actionStub.getCall(0).args[0]).toEqual('Test.txt'); + expect(actionStub.getCall(0).args[1]).toBe(context); + + expect(notifyUpdateListenersSpy.calledTwice).toEqual(true); + expect(notifyUpdateListenersSpy.calledBefore(actionStub)).toEqual(true); + expect(notifyUpdateListenersSpy.calledAfter(actionStub)).toEqual(true); + expect(notifyUpdateListenersSpy.getCall(0).args[0]).toEqual('beforeTriggerAction'); + expect(notifyUpdateListenersSpy.getCall(0).args[1]).toEqual({ + action: fileActions.getActions('all', OCA.Files.FileActions.TYPE_INLINE, OC.PERMISSION_READ)['Test'], + fileName: 'Test.txt', + context: context + }); + expect(notifyUpdateListenersSpy.getCall(1).args[0]).toEqual('afterTriggerAction'); + expect(notifyUpdateListenersSpy.getCall(1).args[1]).toEqual({ + action: fileActions.getActions('all', OCA.Files.FileActions.TYPE_INLINE, OC.PERMISSION_READ)['Test'], + fileName: 'Test.txt', + context: context + }); + }), describe('actions menu', function() { it('shows actions menu inside row when clicking the menu trigger', function() { expect($tr.find('td.filename .fileActionsMenu').length).toEqual(0); diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index 83926b24fee..fc5a6c18f95 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -2489,6 +2489,30 @@ describe('OCA.Files.FileList tests', function() { expect(context.fileActions).toBeDefined(); expect(context.dir).toEqual('/subdir'); }); + it('Clicking on an empty space of the file row will trigger the "Details" action', function() { + var detailsActionStub = sinon.stub(); + fileList.setFiles(testFiles); + // Override the "Details" action set internally by the FileList for + // easier testing. + fileList.fileActions.registerAction({ + mime: 'all', + name: 'Details', + permissions: OC.PERMISSION_NONE, + actionHandler: detailsActionStub + }); + // Ensure that the action works even if fileActions.currentFile is + // not set. + fileList.fileActions.currentFile = null; + var $tr = fileList.findFileEl('One.txt'); + $tr.find('td.filename a.name').click(); + expect(detailsActionStub.calledOnce).toEqual(true); + expect(detailsActionStub.getCall(0).args[0]).toEqual('One.txt'); + var context = detailsActionStub.getCall(0).args[1]; + expect(context.$file.is($tr)).toEqual(true); + expect(context.fileList).toBe(fileList); + expect(context.fileActions).toBe(fileList.fileActions); + expect(context.dir).toEqual('/subdir'); + }); it('redisplays actions when new actions have been registered', function() { var actionStub = sinon.stub(); var readyHandler = sinon.stub(); diff --git a/apps/files_external/lib/Lib/Storage/SMB.php b/apps/files_external/lib/Lib/Storage/SMB.php index 557dafda72c..d8bbe8c4718 100644 --- a/apps/files_external/lib/Lib/Storage/SMB.php +++ b/apps/files_external/lib/Lib/Storage/SMB.php @@ -52,6 +52,7 @@ use OCP\Files\Notify\IChange; use OCP\Files\Notify\IRenameChange; use OCP\Files\Storage\INotifyStorage; use OCP\Files\StorageNotAvailableException; +use OCP\Util; class SMB extends Common implements INotifyStorage { /** @@ -199,18 +200,21 @@ class SMB extends Common implements INotifyStorage { $this->remove($target); $result = $this->share->rename($absoluteSource, $absoluteTarget); } catch (\Exception $e) { + \OC::$server->getLogger()->logException($e, ['level' => Util::WARN]); return false; } unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]); return $result; } - /** - * @param string $path - * @return array - */ public function stat($path) { - $result = $this->formatInfo($this->getFileInfo($path)); + try { + $result = $this->formatInfo($this->getFileInfo($path)); + } catch (ForbiddenException $e) { + return false; + } catch (NotFoundException $e) { + return false; + } if ($this->remoteIsShare() && $this->isRootDir($path)) { $result['mtime'] = $this->shareMTime(); } diff --git a/apps/files_sharing/js/public.js b/apps/files_sharing/js/public.js index 2142dec1218..ae19500080b 100644 --- a/apps/files_sharing/js/public.js +++ b/apps/files_sharing/js/public.js @@ -434,10 +434,13 @@ $(document).ready(function () { $(document).mouseup(function(e) { + var toggle = $('#share-menutoggle'); var container = $('#share-menu'); - // if the target of the click isn't the container nor a descendant of the container - if (!container.is(e.target) && container.has(e.target).length === 0) { + // if the target of the click isn't the menu toggle, nor a descendant of the + // menu toggle, nor the container nor a descendant of the container + if (!toggle.is(e.target) && toggle.has(e.target).length === 0 && + !container.is(e.target) && container.has(e.target).length === 0) { container.removeClass('open'); } }); diff --git a/apps/files_sharing/lib/External/Storage.php b/apps/files_sharing/lib/External/Storage.php index bae24e89e64..638f82f7027 100644 --- a/apps/files_sharing/lib/External/Storage.php +++ b/apps/files_sharing/lib/External/Storage.php @@ -261,7 +261,7 @@ class Storage extends DAV implements ISharedStorage { * @return bool */ private function testRemoteUrl($url) { - $cache = $this->memcacheFactory->create('files_sharing_remote_url'); + $cache = $this->memcacheFactory->createDistributed('files_sharing_remote_url'); if($cache->hasKey($url)) { return (bool)$cache->get($url); } diff --git a/apps/files_sharing/templates/public.php b/apps/files_sharing/templates/public.php index e17595d548b..9d28c178dde 100644 --- a/apps/files_sharing/templates/public.php +++ b/apps/files_sharing/templates/public.php @@ -50,7 +50,7 @@ $maxUploadFilesize = min($upload_max_filesize, $post_max_size); <div class="header-right"> <?php if (!isset($_['hideFileList']) || (isset($_['hideFileList']) && $_['hideFileList'] === false)) { ?> - <a href="#" id="share-menutoggle" class="menutoggle icon-more-white"><span class="share-menutoggle-text"><?php p($l->t('Download')) ?></span></a> + <a id="share-menutoggle" class="menutoggle icon-more-white"><span class="share-menutoggle-text"><?php p($l->t('Download')) ?></span></a> <div id="share-menu" class="popovermenu menu"> <ul> <li> @@ -60,7 +60,7 @@ $maxUploadFilesize = min($upload_max_filesize, $post_max_size); </a> </li> <li> - <a href="#" id="directLink-container"> + <a id="directLink-container"> <span class="icon icon-public"></span> <label for="directLink"><?php p($l->t('Direct link')) ?></label> <input id="directLink" type="text" readonly value="<?php p($_['previewURL']); ?>"> @@ -68,7 +68,7 @@ $maxUploadFilesize = min($upload_max_filesize, $post_max_size); </li> <?php if ($_['server2serversharing']) { ?> <li> - <a href="#" id="save" data-protected="<?php p($_['protected']) ?>" + <a id="save" data-protected="<?php p($_['protected']) ?>" data-owner-display-name="<?php p($_['displayName']) ?>" data-owner="<?php p($_['owner']) ?>" data-name="<?php p($_['filename']) ?>"> <span class="icon icon-external"></span> <span id="save-button"><?php p($l->t('Add to your Nextcloud')) ?></span> diff --git a/apps/theming/lib/ThemingDefaults.php b/apps/theming/lib/ThemingDefaults.php index fa43dd50ccd..05d387e6273 100644 --- a/apps/theming/lib/ThemingDefaults.php +++ b/apps/theming/lib/ThemingDefaults.php @@ -240,7 +240,7 @@ class ThemingDefaults extends \OC_Defaults { * @return array scss variables to overwrite */ public function getScssVariables() { - $cache = $this->cacheFactory->create('theming'); + $cache = $this->cacheFactory->createDistributed('theming'); if ($value = $cache->get('getScssVariables')) { return $value; } @@ -307,7 +307,7 @@ class ThemingDefaults extends \OC_Defaults { * @return bool */ public function shouldReplaceIcons() { - $cache = $this->cacheFactory->create('theming'); + $cache = $this->cacheFactory->createDistributed('theming'); if($value = $cache->get('shouldReplaceIcons')) { return (bool)$value; } @@ -329,7 +329,7 @@ class ThemingDefaults extends \OC_Defaults { private function increaseCacheBuster() { $cacheBusterKey = $this->config->getAppValue('theming', 'cachebuster', '0'); $this->config->setAppValue('theming', 'cachebuster', (int)$cacheBusterKey+1); - $this->cacheFactory->create('theming')->clear('getScssVariables'); + $this->cacheFactory->createDistributed('theming')->clear('getScssVariables'); } /** diff --git a/apps/theming/tests/ThemingDefaultsTest.php b/apps/theming/tests/ThemingDefaultsTest.php index 52bf88e51dd..843c1d34f9e 100644 --- a/apps/theming/tests/ThemingDefaultsTest.php +++ b/apps/theming/tests/ThemingDefaultsTest.php @@ -78,7 +78,7 @@ class ThemingDefaultsTest extends TestCase { $this->defaults = new \OC_Defaults(); $this->cacheFactory ->expects($this->any()) - ->method('create') + ->method('createDistributed') ->with('theming') ->willReturn($this->cache); $this->template = new ThemingDefaults( diff --git a/apps/user_ldap/lib/Connection.php b/apps/user_ldap/lib/Connection.php index 1dcf9b72d7c..bde489e2710 100644 --- a/apps/user_ldap/lib/Connection.php +++ b/apps/user_ldap/lib/Connection.php @@ -100,7 +100,7 @@ class Connection extends LDAPUtility { !is_null($configID)); $memcache = \OC::$server->getMemCacheFactory(); if($memcache->isAvailable()) { - $this->cache = $memcache->create(); + $this->cache = $memcache->createDistributed(); } $helper = new Helper(\OC::$server->getConfig()); $this->doNotValidate = !in_array($this->configPrefix, diff --git a/apps/user_ldap/lib/LDAP.php b/apps/user_ldap/lib/LDAP.php index eafd8eacd06..bdc2f204225 100644 --- a/apps/user_ldap/lib/LDAP.php +++ b/apps/user_ldap/lib/LDAP.php @@ -63,8 +63,8 @@ class LDAP implements ILDAPWrapper { } /** - * @param LDAP $link - * @param LDAP $result + * @param resource $link + * @param resource $result * @param string $cookie * @return bool|LDAP */ @@ -331,6 +331,8 @@ class LDAP implements ILDAPWrapper { //referrals, we switch them off, but then there is AD :) } else if ($errorCode === -1) { throw new ServerNotAvailableException('Lost connection to LDAP server.'); + } else if ($errorCode === 52) { + throw new ServerNotAvailableException('LDAP server is shutting down.'); } else if ($errorCode === 48) { throw new \Exception('LDAP authentication method rejected', $errorCode); } else if ($errorCode === 1) { @@ -339,11 +341,12 @@ class LDAP implements ILDAPWrapper { ldap_get_option($this->curArgs[0], LDAP_OPT_ERROR_STRING, $extended_error); throw new ConstraintViolationException(!empty($extended_error)?$extended_error:$errorMsg, $errorCode); } else { - \OCP\Util::writeLog('user_ldap', - 'LDAP error '.$errorMsg.' (' . - $errorCode.') after calling '. - $this->curFunc, - \OCP\Util::DEBUG); + \OC::$server->getLogger()->debug('LDAP error {message} ({code}) after calling {func}', [ + 'app' => 'user_ldap', + 'message' => $errorMsg, + 'code' => $errorCode, + 'func' => $this->curFunc, + ]); } } diff --git a/apps/user_ldap/lib/Proxy.php b/apps/user_ldap/lib/Proxy.php index d372ff9c026..dc8c6fc77cc 100644 --- a/apps/user_ldap/lib/Proxy.php +++ b/apps/user_ldap/lib/Proxy.php @@ -50,7 +50,7 @@ abstract class Proxy { $this->ldap = $ldap; $memcache = \OC::$server->getMemCacheFactory(); if($memcache->isAvailable()) { - $this->cache = $memcache->create(); + $this->cache = $memcache->createDistributed(); } } diff --git a/core/Command/Db/AddMissingIndices.php b/core/Command/Db/AddMissingIndices.php new file mode 100644 index 00000000000..314bed8ccb1 --- /dev/null +++ b/core/Command/Db/AddMissingIndices.php @@ -0,0 +1,91 @@ +<?php +/** + * @copyright Copyright (c) 2017 Bjoern Schiessle <bjoern@schiessle.org> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + + +namespace OC\Core\Command\Db; + +use OC\DB\SchemaWrapper; +use OCP\IDBConnection; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Class AddMissingIndices + * + * if you added any new indices to the database, this is the right place to add + * it your update routine for existing instances + * + * @package OC\Core\Command\Db + */ +class AddMissingIndices extends Command { + + /** @var IDBConnection */ + private $connection; + + /** + * @param IDBConnection $connection + */ + public function __construct(IDBConnection $connection) { + $this->connection = $connection; + parent::__construct(); + } + + protected function configure() { + $this + ->setName('db:add-missing-indices') + ->setDescription('Add missing indices to the database tables'); + } + + protected function execute(InputInterface $input, OutputInterface $output) { + $this->addShareTableIndicies($output); + + } + + /** + * add missing indices to the share table + * + * @param OutputInterface $output + * @throws \Doctrine\DBAL\Schema\SchemaException + */ + private function addShareTableIndicies(OutputInterface $output) { + + $output->writeln('<info>Check indices of the share table.</info>'); + + $schema = new SchemaWrapper($this->connection); + $updated = false; + + if ($schema->hasTable("share")) { + $table = $schema->getTable("share"); + if (!$table->hasIndex('share_with_index')) { + $output->writeln('<info>Adding additional index to the share table, this can take some time...</info>'); + $table->addIndex(['share_with'], 'share_with_index'); + $this->connection->migrateToSchema($schema->getWrappedSchema()); + $updated = true; + $output->writeln('<info>Share table updated successfully.</info>'); + } + } + + if (!$updated) { + $output->writeln('<info>Done.</info>'); + } + } +} diff --git a/core/Command/Maintenance/Repair.php b/core/Command/Maintenance/Repair.php index 9401dafd26b..71d13cd29f3 100644 --- a/core/Command/Maintenance/Repair.php +++ b/core/Command/Maintenance/Repair.php @@ -86,7 +86,7 @@ class Repair extends Command { $apps = $this->appManager->getInstalledApps(); foreach ($apps as $app) { - if (!$appManager->isEnabledForUser($app)) { + if (!$this->appManager->isEnabledForUser($app)) { continue; } $info = \OC_App::getAppInfo($app); diff --git a/core/Command/Maintenance/UpdateTheme.php b/core/Command/Maintenance/UpdateTheme.php index cf015b82635..2ab66a4ce75 100644 --- a/core/Command/Maintenance/UpdateTheme.php +++ b/core/Command/Maintenance/UpdateTheme.php @@ -57,7 +57,7 @@ class UpdateTheme extends UpdateJS { parent::execute($input, $output); // cleanup image cache - $c = $this->cacheFactory->create('imagePath'); + $c = $this->cacheFactory->createDistributed('imagePath'); $c->clear(''); $output->writeln('<info>Image cache cleared'); } diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php index e87e097e423..e53095a7de7 100644 --- a/core/Controller/LoginController.php +++ b/core/Controller/LoginController.php @@ -179,6 +179,7 @@ class LoginController extends Controller { $parameters['alt_login'] = OC_App::getAlternativeLogIns(); $parameters['rememberLoginState'] = !empty($remember_login) ? $remember_login : 0; + $parameters['hideRemeberLoginState'] = !empty($redirect_url) && $this->session->exists('client.flow.state.token'); if (!is_null($user) && $user !== '') { $parameters['loginName'] = $user; diff --git a/core/Migrations/Version13000Date20170718121200.php b/core/Migrations/Version13000Date20170718121200.php index 0ab777f6de2..e71debfcb4b 100644 --- a/core/Migrations/Version13000Date20170718121200.php +++ b/core/Migrations/Version13000Date20170718121200.php @@ -400,6 +400,7 @@ class Version13000Date20170718121200 extends SimpleMigrationStep { $table->addIndex(['item_type', 'share_type'], 'item_share_type_index'); $table->addIndex(['file_source'], 'file_source_index'); $table->addIndex(['token'], 'token_index'); + $table->addIndex(['share_with'], 'share_with_index'); } if (!$schema->hasTable('jobs')) { diff --git a/core/css/fixes.scss b/core/css/fixes.scss index 3cb89c6599f..0303b4d751a 100644 --- a/core/css/fixes.scss +++ b/core/css/fixes.scss @@ -16,3 +16,10 @@ select { visibility: hidden; } +.ie #header .menu, +.ie .header-left #navigation, +.ie .ui-datepicker, +.ie .ui-timepicker.ui-widget, +.ie #appmenu li span { + box-shadow: 0 1px 10px $color-box-shadow; +} diff --git a/core/css/header.scss b/core/css/header.scss index 21305de0d02..b38c0bcb401 100644 --- a/core/css/header.scss +++ b/core/css/header.scss @@ -76,13 +76,16 @@ .menu { top: 45px; background-color: $color-main-background; - filter: drop-shadow(0 1px 3px $color-box-shadow); + filter: drop-shadow(0 1px 10px $color-box-shadow); border-radius: 0 0 3px 3px; - display: none; box-sizing: border-box; z-index: 2000; position: absolute; + &:not(.popovermenu) { + display: none; + } + /* Dropdown arrow */ &:after { border: 10px solid transparent; @@ -210,7 +213,7 @@ nav { left: -100%; width: 160px; background-color: $color-main-background; - filter: drop-shadow(0 1px 3px $color-box-shadow); + filter: drop-shadow(0 1px 10px $color-box-shadow); &:after { /* position of dropdown arrow */ left: 47%; @@ -408,7 +411,6 @@ nav { #expanddiv { right: 13px; background: $color-main-background; - box-shadow: 0 1px 10px $color-box-shadow; &:after { /* position of dropdown arrow */ right: 13px; @@ -483,7 +485,7 @@ nav { display: none; position: absolute; overflow: visible; - background-color: rgba($color-main-background, .97); + background-color: $color-main-background; white-space: nowrap; border: none; border-radius: $border-radius; @@ -496,7 +498,7 @@ nav { top: 45px; transform: translateX(-50%); padding: 4px 10px; - box-shadow: 0 1px 10px $color-box-shadow; + filter: drop-shadow(0 1px 10px $color-box-shadow); } li:hover span { diff --git a/core/css/mobile.scss b/core/css/mobile.scss index 19518479987..6f1583cb77a 100644 --- a/core/css/mobile.scss +++ b/core/css/mobile.scss @@ -83,9 +83,7 @@ /* position controls for apps with app-navigation */ #app-navigation+#app-content #controls { - left: 0 !important; padding-left: 44px; - width: 100%; } /* .viewer-mode is when text editor, PDF viewer, etc is open */ diff --git a/core/css/styles.scss b/core/css/styles.scss index 5474b41a2b4..1b76e3c68de 100644 --- a/core/css/styles.scss +++ b/core/css/styles.scss @@ -223,29 +223,23 @@ body { #controls { box-sizing: border-box; - position: fixed; - top: 45px; - right: 0; - left: 0; + position: -webkit-sticky; + position: sticky; height: 44px; - width: calc(100% - 250px); padding: 0; margin: 0; background-color: rgba($color-main-background, 0.95); - z-index: 50; + z-index: 55; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; - display: inline-flex; + display: flex; + top: 0; } /* position controls for apps with app-navigation */ -#app-navigation + #app-content #controls { - left: 250px; -} - .viewer-mode #app-navigation + #app-content #controls { left: 0; } diff --git a/core/js/jquery.avatar.js b/core/js/jquery.avatar.js index 54518c75cc7..958f0f9edd7 100644 --- a/core/js/jquery.avatar.js +++ b/core/js/jquery.avatar.js @@ -106,54 +106,39 @@ }); } - // If the displayname is not defined we use the old code path - if (typeof(displayname) === 'undefined') { - $.get(url).always(function(result, status) { - // if there is an error or an object returned (contains user information): - // -> show the fallback placeholder - if (typeof(result) === 'object' || status === 'error') { - if (!hidedefault) { - if (result.data && result.data.displayname) { - $div.imageplaceholder(user, result.data.displayname); - } else { - // User does not exist - setAvatarForUnknownUser($div); - } - } else { - $div.hide(); - } - // else an image is transferred and should be shown - } else { - $div.show(); - if (ie8fix === true) { - $div.html('<img width="' + size + '" height="' + size + '" src="'+url+'#'+Math.floor(Math.random()*1000)+'" alt="">'); - } else { - $div.html('<img width="' + size + '" height="' + size + '" src="'+url+'" alt="">'); - } - } - if(typeof callback === 'function') { - callback(); - } - }); - } else { - // We already have the displayname so set the placeholder (to show at least something) - if (!hidedefault) { - $div.imageplaceholder(displayname); - } + var img = new Image(); + + // If the new image loads successfully set it. + img.onload = function() { + $div.text(''); + $div.append(img); + $div.clearimageplaceholder(); - var img = new Image(); + if(typeof callback === 'function') { + callback(); + } + }; + // Fallback when avatar loading fails: + // Use old placeholder when a displayname attribute is defined, + // otherwise show the unknown user placeholder. + img.onerror = function () { + $div.clearimageplaceholder(); + if (typeof(displayname) !== 'undefined') { + $div.imageplaceholder(user, displayname); + } else { + setAvatarForUnknownUser($div); + $div.removeClass('icon-loading'); + } - // If the new image loads successfully set it. - img.onload = function() { - $div.show(); - $div.text(''); - $div.append(img); - $div.clearimageplaceholder(); - }; + if(typeof callback === 'function') { + callback(); + } + }; - img.width = size; - img.height = size; - img.src = url; - } + $div.addClass('icon-loading'); + $div.show(); + img.width = size; + img.height = size; + img.src = url; }; }(jQuery)); diff --git a/core/js/lostpassword.js b/core/js/lostpassword.js index 446d70d991e..b44962f552e 100644 --- a/core/js/lostpassword.js +++ b/core/js/lostpassword.js @@ -16,6 +16,7 @@ OC.Lostpassword = { $('#lost-password').click(OC.Lostpassword.resetLink); $('#lost-password-back').click(OC.Lostpassword.backToLogin); $('form[name=login]').submit(OC.Lostpassword.onSendLink); + $('#reset-password #submit').click(OC.Lostpassword.resetPassword); OC.Lostpassword.resetButtons(); }, diff --git a/core/js/placeholder.js b/core/js/placeholder.js index f173e738676..5cf7b9095ad 100644 --- a/core/js/placeholder.js +++ b/core/js/placeholder.js @@ -2,7 +2,7 @@ * ownCloud * * @author John Molakvoæ - * @copyright 2016 John Molakvoæ <fremulon@protonmail.com> + * @copyright 2016-2017 John Molakvoæ <skjnldsv@protonmail.com> * @author Morris Jobke * @copyright 2013 Morris Jobke <morris.jobke@gmail.com> * @@ -47,7 +47,7 @@ * <div id="albumart" style="background-color: hsl(123, 90%, 65%); ... ">A</div> * */ - + /* * Alternatively, you can use the prototype function to convert your string to hsl colors: * @@ -156,5 +156,6 @@ this.css('text-align', ''); this.css('line-height', ''); this.css('font-size', ''); + this.removeClass('icon-loading'); }; }(jQuery)); diff --git a/core/js/tests/specs/jquery.avatarSpec.js b/core/js/tests/specs/jquery.avatarSpec.js index b9351d2a8a0..bdd1fdcc163 100644 --- a/core/js/tests/specs/jquery.avatarSpec.js +++ b/core/js/tests/specs/jquery.avatarSpec.js @@ -19,6 +19,13 @@ describe('jquery.avatar tests', function() { devicePixelRatio = window.devicePixelRatio; window.devicePixelRatio = 1; + + spyOn(window, 'Image').and.returnValue({ + onload: function() { + }, + onerror: function() { + } + }); }); afterEach(function() { @@ -39,6 +46,9 @@ describe('jquery.avatar tests', function() { $div.height(9); $div.avatar('foo'); + expect(window.Image).toHaveBeenCalled(); + window.Image().onerror(); + expect($div.height()).toEqual(9); expect($div.width()).toEqual(9); }); @@ -47,6 +57,9 @@ describe('jquery.avatar tests', function() { $div.data('size', 10); $div.avatar('foo'); + expect(window.Image).toHaveBeenCalled(); + window.Image().onerror(); + expect($div.height()).toEqual(10); expect($div.width()).toEqual(10); }); @@ -55,6 +68,9 @@ describe('jquery.avatar tests', function() { it('defined', function() { $div.avatar('foo', 8); + expect(window.Image).toHaveBeenCalled(); + window.Image().onerror(); + expect($div.height()).toEqual(8); expect($div.width()).toEqual(8); }); @@ -73,16 +89,10 @@ describe('jquery.avatar tests', function() { describe('no avatar', function() { it('show placeholder for existing user', function() { spyOn($div, 'imageplaceholder'); - $div.avatar('foo'); - - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({ - data: {displayname: 'bar'} - }) - ); + $div.avatar('foo', undefined, undefined, undefined, undefined, 'bar'); + expect(window.Image).toHaveBeenCalled(); + window.Image().onerror(); expect($div.imageplaceholder).toHaveBeenCalledWith('foo', 'bar'); }); @@ -91,32 +101,23 @@ describe('jquery.avatar tests', function() { spyOn($div, 'css'); $div.avatar('foo'); - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({ - data: {} - }) - ); + expect(window.Image).toHaveBeenCalled(); + window.Image().onerror(); expect($div.imageplaceholder).toHaveBeenCalledWith('?'); expect($div.css).toHaveBeenCalledWith('background-color', '#b9b9b9'); }); - it('show no placeholder', function() { + it('show no placeholder is ignored', function() { spyOn($div, 'imageplaceholder'); + spyOn($div, 'css'); $div.avatar('foo', undefined, undefined, true); - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({ - data: {} - }) - ); + expect(window.Image).toHaveBeenCalled(); + window.Image().onerror(); - expect($div.imageplaceholder.calls.any()).toEqual(false); - expect($div.css('display')).toEqual('none'); + expect($div.imageplaceholder).toHaveBeenCalledWith('?'); + expect($div.css).toHaveBeenCalledWith('background-color', '#b9b9b9'); }); }); @@ -129,24 +130,24 @@ describe('jquery.avatar tests', function() { window.devicePixelRatio = 1; $div.avatar('foo', 32); - expect(fakeServer.requests[0].method).toEqual('GET'); - expect(fakeServer.requests[0].url).toEqual('http://localhost/index.php/avatar/foo/32'); + expect(window.Image).toHaveBeenCalled(); + expect(window.Image().src).toEqual('http://localhost/index.php/avatar/foo/32'); }); it('high DPI icon', function() { window.devicePixelRatio = 4; $div.avatar('foo', 32); - expect(fakeServer.requests[0].method).toEqual('GET'); - expect(fakeServer.requests[0].url).toEqual('http://localhost/index.php/avatar/foo/128'); + expect(window.Image).toHaveBeenCalled(); + expect(window.Image().src).toEqual('http://localhost/index.php/avatar/foo/128'); }); it('high DPI icon round up size', function() { window.devicePixelRatio = 1.9; $div.avatar('foo', 32); - expect(fakeServer.requests[0].method).toEqual('GET'); - expect(fakeServer.requests[0].url).toEqual('http://localhost/index.php/avatar/foo/61'); + expect(window.Image).toHaveBeenCalled(); + expect(window.Image().src).toEqual('http://localhost/index.php/avatar/foo/61'); }); }); @@ -158,17 +159,12 @@ describe('jquery.avatar tests', function() { it('default (no ie8 fix)', function() { $div.avatar('foo', 32); - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'image/jpeg' }, - '' - ); + expect(window.Image).toHaveBeenCalled(); + window.Image().onload(); - var img = $div.children('img')[0]; - - expect(img.height).toEqual(32); - expect(img.width).toEqual(32); - expect(img.src).toEqual('http://localhost/index.php/avatar/foo/32'); + expect(window.Image().height).toEqual(32); + expect(window.Image().width).toEqual(32); + expect(window.Image().src).toEqual('http://localhost/index.php/avatar/foo/32'); }); it('default high DPI icon', function() { @@ -176,37 +172,23 @@ describe('jquery.avatar tests', function() { $div.avatar('foo', 32); - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'image/jpeg' }, - '' - ); - - var img = $div.children('img')[0]; + expect(window.Image).toHaveBeenCalled(); + window.Image().onload(); - expect(img.height).toEqual(32); - expect(img.width).toEqual(32); - expect(img.src).toEqual('http://localhost/index.php/avatar/foo/61'); + expect(window.Image().height).toEqual(32); + expect(window.Image().width).toEqual(32); + expect(window.Image().src).toEqual('http://localhost/index.php/avatar/foo/61'); }); - it('with ie8 fix', function() { - sinon.stub(Math, 'random').callsFake(function() { - return 0.5; - }); - + it('with ie8 fix (ignored)', function() { $div.avatar('foo', 32, true); - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'image/jpeg' }, - '' - ); + expect(window.Image).toHaveBeenCalled(); + window.Image().onload(); - var img = $div.children('img')[0]; - - expect(img.height).toEqual(32); - expect(img.width).toEqual(32); - expect(img.src).toEqual('http://localhost/index.php/avatar/foo/32#500'); + expect(window.Image().height).toEqual(32); + expect(window.Image().width).toEqual(32); + expect(window.Image().src).toEqual('http://localhost/index.php/avatar/foo/32'); }); it('unhide div', function() { @@ -214,11 +196,12 @@ describe('jquery.avatar tests', function() { $div.avatar('foo', 32); - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'image/jpeg' }, - '' - ); + expect(window.Image).toHaveBeenCalled(); + window.Image().onload(); + + expect(window.Image().height).toEqual(32); + expect(window.Image().width).toEqual(32); + expect(window.Image().src).toEqual('http://localhost/index.php/avatar/foo/32'); expect($div.css('display')).toEqual('block'); }); @@ -232,12 +215,12 @@ describe('jquery.avatar tests', function() { observer.callback(); }); - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'image/jpeg' }, - '' - ); + expect(window.Image).toHaveBeenCalled(); + window.Image().onload(); + expect(window.Image().height).toEqual(32); + expect(window.Image().width).toEqual(32); + expect(window.Image().src).toEqual('http://localhost/index.php/avatar/foo/32'); expect(observer.callback).toHaveBeenCalled(); }); }); diff --git a/core/register_command.php b/core/register_command.php index 60e151a5f2c..372d775dc14 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -90,6 +90,7 @@ if (\OC::$server->getConfig()->getSystemValue('installed', false)) { $application->add(new OC\Core\Command\Db\ConvertType(\OC::$server->getConfig(), new \OC\DB\ConnectionFactory(\OC::$server->getSystemConfig()))); $application->add(new OC\Core\Command\Db\ConvertMysqlToMB4(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection(), \OC::$server->getURLGenerator(), \OC::$server->getLogger())); $application->add(new OC\Core\Command\Db\ConvertFilecacheBigInt(\OC::$server->getDatabaseConnection())); + $application->add(new OC\Core\Command\Db\AddMissingIndices(\OC::$server->getDatabaseConnection())); $application->add(new OC\Core\Command\Db\Migrations\StatusCommand(\OC::$server->getDatabaseConnection())); $application->add(new OC\Core\Command\Db\Migrations\MigrateCommand(\OC::$server->getDatabaseConnection())); $application->add(new OC\Core\Command\Db\Migrations\GenerateCommand(\OC::$server->getDatabaseConnection())); diff --git a/core/templates/login.php b/core/templates/login.php index 82827bbef03..d28c92e36ef 100644 --- a/core/templates/login.php +++ b/core/templates/login.php @@ -70,6 +70,7 @@ script('core', 'merged-login'); <?php } ?> <div class="login-additional"> + <?php if (!$_['hideRemeberLoginState']) { ?> <div class="remember-login-container"> <?php if ($_['rememberLoginState'] === 0) { ?> <input type="checkbox" name="remember_login" value="1" id="remember_login" class="checkbox checkbox--white"> @@ -78,6 +79,7 @@ script('core', 'merged-login'); <?php } ?> <label for="remember_login"><?php p($l->t('Stay logged in')); ?></label> </div> + <?php } ?> <?php if (!empty($_['canResetPassword'])) { ?> <div class="lost-password-container"> <a id="lost-password" href="<?php p($_['resetPasswordLink']); ?>"> diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index a3b85349274..738054cd377 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -459,6 +459,7 @@ return array( 'OC\\Core\\Command\\Config\\System\\DeleteConfig' => $baseDir . '/core/Command/Config/System/DeleteConfig.php', 'OC\\Core\\Command\\Config\\System\\GetConfig' => $baseDir . '/core/Command/Config/System/GetConfig.php', 'OC\\Core\\Command\\Config\\System\\SetConfig' => $baseDir . '/core/Command/Config/System/SetConfig.php', + 'OC\\Core\\Command\\Db\\AddMissingIndices' => $baseDir . '/core/Command/Db/AddMissingIndices.php', 'OC\\Core\\Command\\Db\\ConvertFilecacheBigInt' => $baseDir . '/core/Command/Db/ConvertFilecacheBigInt.php', 'OC\\Core\\Command\\Db\\ConvertMysqlToMB4' => $baseDir . '/core/Command/Db/ConvertMysqlToMB4.php', 'OC\\Core\\Command\\Db\\ConvertType' => $baseDir . '/core/Command/Db/ConvertType.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 408a30e2540..7ffbd4c7882 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -489,6 +489,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Command\\Config\\System\\DeleteConfig' => __DIR__ . '/../../..' . '/core/Command/Config/System/DeleteConfig.php', 'OC\\Core\\Command\\Config\\System\\GetConfig' => __DIR__ . '/../../..' . '/core/Command/Config/System/GetConfig.php', 'OC\\Core\\Command\\Config\\System\\SetConfig' => __DIR__ . '/../../..' . '/core/Command/Config/System/SetConfig.php', + 'OC\\Core\\Command\\Db\\AddMissingIndices' => __DIR__ . '/../../..' . '/core/Command/Db/AddMissingIndices.php', 'OC\\Core\\Command\\Db\\ConvertFilecacheBigInt' => __DIR__ . '/../../..' . '/core/Command/Db/ConvertFilecacheBigInt.php', 'OC\\Core\\Command\\Db\\ConvertMysqlToMB4' => __DIR__ . '/../../..' . '/core/Command/Db/ConvertMysqlToMB4.php', 'OC\\Core\\Command\\Db\\ConvertType' => __DIR__ . '/../../..' . '/core/Command/Db/ConvertType.php', diff --git a/lib/private/App/AppManager.php b/lib/private/App/AppManager.php index 6be892b7f49..e7d4668931c 100644 --- a/lib/private/App/AppManager.php +++ b/lib/private/App/AppManager.php @@ -306,7 +306,7 @@ class AppManager implements IAppManager { * Clear the cached list of apps when enabling/disabling an app */ public function clearAppsCache() { - $settingsMemCache = $this->memCacheFactory->create('settings'); + $settingsMemCache = $this->memCacheFactory->createDistributed('settings'); $settingsMemCache->clear('listApps'); } diff --git a/lib/private/App/AppStore/Fetcher/Fetcher.php b/lib/private/App/AppStore/Fetcher/Fetcher.php index 5ce64671ffa..8bf9ca15349 100644 --- a/lib/private/App/AppStore/Fetcher/Fetcher.php +++ b/lib/private/App/AppStore/Fetcher/Fetcher.php @@ -36,6 +36,7 @@ use OCP\Files\NotFoundException; use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\ILogger; +use OCP\Util; abstract class Fetcher { const INVALIDATE_AFTER_SECONDS = 300; @@ -170,7 +171,7 @@ abstract class Fetcher { $file->putContent(json_encode($responseJson)); return json_decode($file->getContent(), true)['data']; } catch (ConnectException $e) { - $this->logger->logException($e, ['app' => 'appstoreFetcher']); + $this->logger->logException($e, ['app' => 'appstoreFetcher', 'level' => Util::INFO, 'message' => 'Could not connect to appstore']); return []; } catch (\Exception $e) { return []; diff --git a/lib/private/AppFramework/Http/Request.php b/lib/private/AppFramework/Http/Request.php index 072dd9f172f..77ecb02165b 100644 --- a/lib/private/AppFramework/Http/Request.php +++ b/lib/private/AppFramework/Http/Request.php @@ -406,6 +406,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { if ($this->method === 'PUT' && $this->getHeader('Content-Length') !== 0 && $this->getHeader('Content-Length') !== null + && $this->getHeader('Content-Length') !== '' && strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') === false && strpos($this->getHeader('Content-Type'), 'application/json') === false ) { diff --git a/lib/private/Avatar.php b/lib/private/Avatar.php index 5893daa1804..afa9118c509 100644 --- a/lib/private/Avatar.php +++ b/lib/private/Avatar.php @@ -141,6 +141,7 @@ class Avatar implements IAvatar { try { $generated = $this->folder->getFile('generated'); + $this->config->setUserValue($this->user->getUID(), 'avatar', 'generated', 'false'); $generated->delete(); } catch (NotFoundException $e) { // @@ -161,6 +162,7 @@ class Avatar implements IAvatar { foreach ($avatars as $avatar) { $avatar->delete(); } + $this->config->setUserValue($this->user->getUID(), 'avatar', 'generated', 'true'); $this->user->triggerChange('avatar', ''); } @@ -177,6 +179,7 @@ class Avatar implements IAvatar { $ext = 'png'; $this->folder->newFile('generated'); + $this->config->setUserValue($this->user->getUID(), 'avatar', 'generated', 'true'); } if ($size === -1) { @@ -393,4 +396,18 @@ class Avatar implements IAvatar { return array(round($r * 255), round($g * 255), round($b * 255)); } + public function userChanged($feature, $oldValue, $newValue) { + // We only change the avatar on display name changes + if ($feature !== 'displayName') { + return; + } + + // If the avatar is not generated (so an uploaded image) we skip this + if (!$this->folder->fileExists('generated')) { + return; + } + + $this->remove(); + } + } diff --git a/lib/private/Collaboration/Collaborators/MailPlugin.php b/lib/private/Collaboration/Collaborators/MailPlugin.php index d28bd3692a4..2e946c4a872 100644 --- a/lib/private/Collaboration/Collaborators/MailPlugin.php +++ b/lib/private/Collaboration/Collaborators/MailPlugin.php @@ -30,10 +30,13 @@ use OCP\Collaboration\Collaborators\SearchResultType; use OCP\Contacts\IManager; use OCP\Federation\ICloudIdManager; use OCP\IConfig; +use OCP\IGroupManager; +use OCP\IUserSession; use OCP\Share; class MailPlugin implements ISearchPlugin { protected $shareeEnumeration; + protected $shareWithGroupOnly; /** @var IManager */ private $contactsManager; @@ -42,12 +45,21 @@ class MailPlugin implements ISearchPlugin { /** @var IConfig */ private $config; - public function __construct(IManager $contactsManager, ICloudIdManager $cloudIdManager, IConfig $config) { + /** @var IGroupManager */ + private $groupManager; + + /** @var IUserSession */ + private $userSession; + + public function __construct(IManager $contactsManager, ICloudIdManager $cloudIdManager, IConfig $config, IGroupManager $groupManager, IUserSession $userSession) { $this->contactsManager = $contactsManager; $this->cloudIdManager = $cloudIdManager; $this->config = $config; + $this->groupManager = $groupManager; + $this->userSession = $userSession; $this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; + $this->shareWithGroupOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes'; } /** @@ -77,6 +89,22 @@ class MailPlugin implements ISearchPlugin { $exactEmailMatch = strtolower($emailAddress) === $lowerSearch; if (isset($contact['isLocalSystemBook'])) { + if ($this->shareWithGroupOnly) { + /* + * Check if the user may share with the user associated with the e-mail of the just found contact + */ + $userGroups = $this->groupManager->getUserGroupIds($this->userSession->getUser()); + $found = false; + foreach ($userGroups as $userGroup) { + if ($this->groupManager->isInGroup($contact['UID'], $userGroup)) { + $found = true; + break; + } + } + if (!$found) { + continue; + } + } if ($exactEmailMatch) { try { $cloud = $this->cloudIdManager->resolveCloudId($contact['CLOUD'][0]); diff --git a/lib/private/Console/TimestampFormatter.php b/lib/private/Console/TimestampFormatter.php index 66dd38e6ac3..9ced9e18b31 100644 --- a/lib/private/Console/TimestampFormatter.php +++ b/lib/private/Console/TimestampFormatter.php @@ -31,6 +31,9 @@ class TimestampFormatter implements OutputFormatterInterface { /** @var IConfig */ protected $config; + /** @var OutputFormatterInterface */ + protected $formatter; + /** * @param IConfig $config * @param OutputFormatterInterface $formatter @@ -75,7 +78,7 @@ class TimestampFormatter implements OutputFormatterInterface { * @return bool */ public function hasStyle($name) { - $this->formatter->hasStyle($name); + return $this->formatter->hasStyle($name); } /** @@ -83,6 +86,7 @@ class TimestampFormatter implements OutputFormatterInterface { * * @param string $name * @return OutputFormatterStyleInterface + * @throws \InvalidArgumentException When style isn't defined */ public function getStyle($name) { return $this->formatter->getStyle($name); diff --git a/lib/private/Files/ObjectStore/Swift.php b/lib/private/Files/ObjectStore/Swift.php index ecfbe136e4c..629fb3ba7ff 100644 --- a/lib/private/Files/ObjectStore/Swift.php +++ b/lib/private/Files/ObjectStore/Swift.php @@ -82,7 +82,7 @@ class Swift implements IObjectStore { } $cacheFactory = \OC::$server->getMemCacheFactory(); - $this->memcache = $cacheFactory->create('swift::' . $cacheKey); + $this->memcache = $cacheFactory->createDistributed('swift::' . $cacheKey); $this->params = $params; } diff --git a/lib/private/Files/Storage/Common.php b/lib/private/Files/Storage/Common.php index 715b7b18499..56d683ffa25 100644 --- a/lib/private/Files/Storage/Common.php +++ b/lib/private/Files/Storage/Common.php @@ -229,6 +229,9 @@ abstract class Common implements Storage, ILockingStorage { $source = $this->fopen($path1, 'r'); $target = $this->fopen($path2, 'w'); list(, $result) = \OC_Helper::streamCopy($source, $target); + if (!$result) { + \OC::$server->getLogger()->warning("Failed to write data while copying $path1 to $path2"); + } $this->removeCachedFile($path2); return $result; } diff --git a/lib/private/Installer.php b/lib/private/Installer.php index eb1f8a456bf..4dcf5a8dad9 100644 --- a/lib/private/Installer.php +++ b/lib/private/Installer.php @@ -395,6 +395,10 @@ class Installer { return false; } + if ($this->isInstalledFromGit($appId) === true) { + return false; + } + if ($this->apps === null) { $this->apps = $this->appFetcher->get(); } @@ -415,6 +419,22 @@ class Installer { } /** + * Check if app has been installed from git + * @param string $name name of the application to remove + * @return boolean + * + * The function will check if the path contains a .git folder + */ + private function isInstalledFromGit($appId) { + $app = \OC_App::findAppInDirectories($appId); + if($app === false) { + return false; + } + $basedir = $app['path'].'/'.$appId; + return file_exists($basedir.'/.git/'); + } + + /** * Check if app is already downloaded * @param string $name name of the application to remove * @return boolean diff --git a/lib/private/IntegrityCheck/Checker.php b/lib/private/IntegrityCheck/Checker.php index ee7e35550a6..771ac891ab4 100644 --- a/lib/private/IntegrityCheck/Checker.php +++ b/lib/private/IntegrityCheck/Checker.php @@ -87,7 +87,7 @@ class Checker { $this->fileAccessHelper = $fileAccessHelper; $this->appLocator = $appLocator; $this->config = $config; - $this->cache = $cacheFactory->create(self::CACHE_KEY); + $this->cache = $cacheFactory->createDistributed(self::CACHE_KEY); $this->appManager = $appManager; $this->tempManager = $tempManager; } diff --git a/lib/private/OCS/DiscoveryService.php b/lib/private/OCS/DiscoveryService.php index e547747da25..4425947c55d 100644 --- a/lib/private/OCS/DiscoveryService.php +++ b/lib/private/OCS/DiscoveryService.php @@ -46,7 +46,7 @@ class DiscoveryService implements IDiscoveryService { public function __construct(ICacheFactory $cacheFactory, IClientService $clientService ) { - $this->cache = $cacheFactory->create('ocs-discovery'); + $this->cache = $cacheFactory->createDistributed('ocs-discovery'); $this->client = $clientService->newClient(); } diff --git a/lib/private/Security/RateLimiting/Backend/MemoryCache.php b/lib/private/Security/RateLimiting/Backend/MemoryCache.php index 212df664c17..700fa624ed4 100644 --- a/lib/private/Security/RateLimiting/Backend/MemoryCache.php +++ b/lib/private/Security/RateLimiting/Backend/MemoryCache.php @@ -45,7 +45,7 @@ class MemoryCache implements IBackend { */ public function __construct(ICacheFactory $cacheFactory, ITimeFactory $timeFactory) { - $this->cache = $cacheFactory->create(__CLASS__); + $this->cache = $cacheFactory->createDistributed(__CLASS__); $this->timeFactory = $timeFactory; } diff --git a/lib/private/Server.php b/lib/private/Server.php index 6898e93e3bb..4a851d67226 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -394,9 +394,10 @@ class Server extends ServerContainer implements IServerContainer { $userSession->listen('\OC\User', 'logout', function () { \OC_Hook::emit('OC_User', 'logout', array()); }); - $userSession->listen('\OC\User', 'changeUser', function ($user, $feature, $value, $oldValue) { + $userSession->listen('\OC\User', 'changeUser', function ($user, $feature, $value, $oldValue) use ($dispatcher) { /** @var $user \OC\User\User */ \OC_Hook::emit('OC_User', 'changeUser', array('run' => true, 'user' => $user, 'feature' => $feature, 'value' => $value, 'old_value' => $oldValue)); + $dispatcher->dispatch('OCP\IUser::changeUser', new GenericEvent($user, ['feature' => $feature, 'oldValue' => $oldValue, 'value' => $value])); }); return $userSession; }); @@ -952,7 +953,7 @@ class Server extends ServerContainer implements IServerContainer { $c->getConfig(), $c->getThemingDefaults(), \OC::$SERVERROOT, - $cacheFactory->create('SCSS') + $cacheFactory->createDistributed('SCSS') ); }); $this->registerService(EventDispatcher::class, function () { @@ -1093,16 +1094,6 @@ class Server extends ServerContainer implements IServerContainer { return new CloudIdManager(); }); - /* To trick DI since we don't extend the DIContainer here */ - $this->registerService(CleanPreviewsBackgroundJob::class, function (Server $c) { - return new CleanPreviewsBackgroundJob( - $c->getRootFolder(), - $c->getLogger(), - $c->getJobList(), - new TimeFactory() - ); - }); - $this->registerAlias(\OCP\AppFramework\Utility\IControllerMethodReflector::class, \OC\AppFramework\Utility\ControllerMethodReflector::class); $this->registerAlias('ControllerMethodReflector', \OCP\AppFramework\Utility\IControllerMethodReflector::class); @@ -1185,6 +1176,22 @@ class Server extends ServerContainer implements IServerContainer { $logger->info('Could not cleanup avatar of ' . $user->getUID()); } }); + + $dispatcher->addListener('OCP\IUser::changeUser', function (GenericEvent $e) { + $manager = $this->getAvatarManager(); + /** @var IUser $user */ + $user = $e->getSubject(); + $feature = $e->getArgument('feature'); + $oldValue = $e->getArgument('oldValue'); + $value = $e->getArgument('value'); + + try { + $avatar = $manager->getAvatar($user->getUID()); + $avatar->userChanged($feature, $oldValue, $value); + } catch (NotFoundException $e) { + // no avatar to remove + } + }); } /** diff --git a/lib/private/Template/CSSResourceLocator.php b/lib/private/Template/CSSResourceLocator.php index 3c30a9d3356..5ca05d1b953 100644 --- a/lib/private/Template/CSSResourceLocator.php +++ b/lib/private/Template/CSSResourceLocator.php @@ -108,7 +108,7 @@ class CSSResourceLocator extends ResourceLocator { if($this->scssCacher !== null) { if($this->scssCacher->process($root, $file, $app)) { - $this->append($root, $this->scssCacher->getCachedSCSS($app, $file), false, true, true); + $this->append($root, $this->scssCacher->getCachedSCSS($app, $file), \OC::$WEBROOT, true, true); return true; } else { $this->logger->warning('Failed to compile and/or save '.$root.'/'.$file, ['app' => 'core']); @@ -145,7 +145,7 @@ class CSSResourceLocator extends ResourceLocator { } } - $this->resources[] = array($webRoot? : '/', $webRoot, $file); + $this->resources[] = array($webRoot? : \OC::$WEBROOT, $webRoot, $file); } } } diff --git a/lib/private/Template/JSConfigHelper.php b/lib/private/Template/JSConfigHelper.php index 60ac4bfecb0..551fc3b9b0d 100644 --- a/lib/private/Template/JSConfigHelper.php +++ b/lib/private/Template/JSConfigHelper.php @@ -254,6 +254,7 @@ class JSConfigHelper { $array['oc_userconfig'] = json_encode([ 'avatar' => [ 'version' => (int)$this->config->getUserValue($uid, 'avatar', 'version', 0), + 'generated' => $this->config->getUserValue($uid, 'avatar', 'generated', 'true') === 'true', ] ]); } diff --git a/lib/private/Template/SCSSCacher.php b/lib/private/Template/SCSSCacher.php index 8f6cb85a120..a4604425544 100644 --- a/lib/private/Template/SCSSCacher.php +++ b/lib/private/Template/SCSSCacher.php @@ -102,8 +102,7 @@ class SCSSCacher { $fileNameCSS = $this->prependBaseurlPrefix(str_replace('.scss', '.css', $fileNameSCSS)); $path = implode('/', $path); - - $webDir = substr($path, strlen($this->serverRoot)+1); + $webDir = $this->getWebDir($path, $app, $this->serverRoot, \OC::$WEBROOT); try { $folder = $this->appData->getFolder($app); @@ -188,7 +187,7 @@ class SCSSCacher { $scss = new Compiler(); $scss->setImportPaths([ $path, - \OC::$SERVERROOT . '/core/css/', + $this->serverRoot . '/core/css/', ]); // Continue after throw $scss->setIgnoreErrors(true); @@ -283,12 +282,7 @@ class SCSSCacher { */ private function rebaseUrls($css, $webDir) { $re = '/url\([\'"]([\.\w?=\/-]*)[\'"]\)/x'; - // OC\Route\Router:75 - if(($this->config->getSystemValue('htaccess.IgnoreFrontController', false) === true || getenv('front_controller_active') === 'true')) { - $subst = 'url(\'../../'.$webDir.'/$1\')'; - } else { - $subst = 'url(\'../../../'.$webDir.'/$1\')'; - } + $subst = 'url(\''.$webDir.'/$1\')'; return preg_replace($re, $subst, $css); } @@ -315,4 +309,23 @@ class SCSSCacher { $frontendController = ($this->config->getSystemValue('htaccess.IgnoreFrontController', false) === true || getenv('front_controller_active') === 'true'); return substr(md5($this->urlGenerator->getBaseUrl() . $frontendController), 0, 8) . '-' . $cssFile; } + + /** + * Get WebDir root + * @param string $path the css file path + * @param string $appName the app name + * @param string $serverRoot the server root path + * @param string $webRoot the nextcloud installation root path + * @return string the webDir + */ + private function getWebDir($path, $appName, $serverRoot, $webRoot) { + // Detect if path is within server root AND if path is within an app path + if ( strpos($path, $serverRoot) === false && $appWebPath = \OC_App::getAppWebPath($appName)) { + // Get the file path within the app directory + $appDirectoryPath = explode($appName, $path)[1]; + // Remove the webroot + return str_replace($webRoot, '', $appWebPath.$appDirectoryPath); + } + return $webRoot.substr($path, strlen($serverRoot)); + } } diff --git a/lib/private/TemplateLayout.php b/lib/private/TemplateLayout.php index 997980aa5d7..264c10f5f1b 100644 --- a/lib/private/TemplateLayout.php +++ b/lib/private/TemplateLayout.php @@ -291,7 +291,7 @@ class TemplateLayout extends \OC_Template { new JSCombiner( \OC::$server->getAppDataDir('js'), \OC::$server->getURLGenerator(), - \OC::$server->getMemCacheFactory()->create('JS'), + \OC::$server->getMemCacheFactory()->createDistributed('JS'), \OC::$server->getSystemConfig(), \OC::$server->getLogger() ) diff --git a/lib/private/URLGenerator.php b/lib/private/URLGenerator.php index 6fd22b99a6b..f7d80d41b4f 100644 --- a/lib/private/URLGenerator.php +++ b/lib/private/URLGenerator.php @@ -151,7 +151,7 @@ class URLGenerator implements IURLGenerator { * Returns the path to the image. */ public function imagePath($app, $image) { - $cache = $this->cacheFactory->create('imagePath-'.md5($this->getBaseUrl()).'-'); + $cache = $this->cacheFactory->createDistributed('imagePath-'.md5($this->getBaseUrl()).'-'); $cacheKey = $app.'-'.$image; if($key = $cache->get($cacheKey)) { return $key; diff --git a/lib/private/Updater/VersionCheck.php b/lib/private/Updater/VersionCheck.php index 6774ef307b5..c7b829c9ec5 100644 --- a/lib/private/Updater/VersionCheck.php +++ b/lib/private/Updater/VersionCheck.php @@ -101,12 +101,10 @@ class VersionCheck { } else { libxml_clear_errors(); } - } else { - $data = []; } // Cache the result - $this->config->setAppValue('core', 'lastupdateResult', json_encode($data)); + $this->config->setAppValue('core', 'lastupdateResult', json_encode($tmp)); return $tmp; } diff --git a/lib/private/legacy/db.php b/lib/private/legacy/db.php index da21729f123..6e487e25ad5 100644 --- a/lib/private/legacy/db.php +++ b/lib/private/legacy/db.php @@ -105,11 +105,11 @@ class OC_DB { * @param mixed $stmt OC_DB_StatementWrapper, * an array with 'sql' and optionally 'limit' and 'offset' keys * .. or a simple sql query string - * @param array|null $parameters + * @param array $parameters * @return OC_DB_StatementWrapper * @throws \OC\DatabaseException */ - static public function executeAudited( $stmt, array $parameters = null) { + static public function executeAudited( $stmt, array $parameters = []) { if (is_string($stmt)) { // convert to an array with 'sql' if (stripos($stmt, 'LIMIT') !== false) { //OFFSET requires LIMIT, so we only need to check for LIMIT diff --git a/lib/private/legacy/helper.php b/lib/private/legacy/helper.php index e611b4d0732..8bbfb235ee2 100644 --- a/lib/private/legacy/helper.php +++ b/lib/private/legacy/helper.php @@ -497,7 +497,7 @@ class OC_Helper { * @return null|string */ public static function findBinaryPath($program) { - $memcache = \OC::$server->getMemCacheFactory()->create('findBinaryPath'); + $memcache = \OC::$server->getMemCacheFactory()->createDistributed('findBinaryPath'); if ($memcache->hasKey($program)) { return $memcache->get($program); } diff --git a/lib/public/IAvatar.php b/lib/public/IAvatar.php index 369cafa00c1..a6731b63be9 100644 --- a/lib/public/IAvatar.php +++ b/lib/public/IAvatar.php @@ -77,4 +77,10 @@ interface IAvatar { * @since 9.0.0 */ public function getFile($size); + + /** + * Handle a changed user + * @since 13.0.0 + */ + public function userChanged($feature, $oldValue, $newValue); } diff --git a/settings/Activity/SecurityProvider.php b/settings/Activity/SecurityProvider.php index f0789842e82..680881b6e31 100644 --- a/settings/Activity/SecurityProvider.php +++ b/settings/Activity/SecurityProvider.php @@ -53,7 +53,7 @@ class SecurityProvider implements IProvider { throw new InvalidArgumentException(); } - $l = $this->l10n->get('core', $language); + $l = $this->l10n->get('settings', $language); switch ($event->getSubject()) { case 'twofactor_success': diff --git a/settings/ajax/uninstallapp.php b/settings/ajax/uninstallapp.php index b4a2468bd2a..a932e2d79e9 100644 --- a/settings/ajax/uninstallapp.php +++ b/settings/ajax/uninstallapp.php @@ -43,8 +43,8 @@ $appId = OC_App::cleanAppId($appId); $result = OC_App::removeApp($appId); if($result !== false) { // FIXME: Clear the cache - move that into some sane helper method - \OC::$server->getMemCacheFactory()->create('settings')->remove('listApps-0'); - \OC::$server->getMemCacheFactory()->create('settings')->remove('listApps-1'); + \OC::$server->getMemCacheFactory()->createDistributed('settings')->remove('listApps-0'); + \OC::$server->getMemCacheFactory()->createDistributed('settings')->remove('listApps-1'); OC_JSON::success(array('data' => array('appid' => $appId))); } else { $l = \OC::$server->getL10N('settings'); diff --git a/settings/js/apps.js b/settings/js/apps.js index 6406e37cbcb..0a6e86ed701 100644 --- a/settings/js/apps.js +++ b/settings/js/apps.js @@ -535,20 +535,20 @@ OC.Settings.Apps = OC.Settings.Apps || { showEmptyUpdates: function() { $('#apps-list').addClass('hidden'); - $('#apps-list-empty').removeClass('hidden').find('h2').text(t('settings', 'No app updates available')); + $('#apps-list-empty').removeClass('hidden').find('h2').text(t('settings', 'App up to date')); $('#app-list-empty-icon').removeClass('icon-search').addClass('icon-download'); }, updateApp:function(appId, element) { var oldButtonText = element.val(); - element.val(t('settings','Updating....')); + element.val(t('settings','Upgrading …')); OC.Settings.Apps.hideErrorMessage(appId); $.post(OC.filePath('settings','ajax','updateapp.php'),{appid:appId},function(result) { if(!result || result.status !== 'success') { if (result.data && result.data.message) { OC.Settings.Apps.showErrorMessage(appId, result.data.message); } else { - OC.Settings.Apps.showErrorMessage(appId, t('settings','Error while updating app')); + OC.Settings.Apps.showErrorMessage(appId, t('settings','Could not upgrade app')); } element.val(oldButtonText); } @@ -584,7 +584,7 @@ OC.Settings.Apps = OC.Settings.Apps || { element.val(t('settings','Removing …')); $.post(OC.filePath('settings','ajax','uninstallapp.php'),{appid:appId},function(result) { if(!result || result.status !== 'success') { - OC.Settings.Apps.showErrorMessage(appId, t('settings','Error while removing app')); + OC.Settings.Apps.showErrorMessage(appId, t('settings','Could not remove app')); element.val(t('settings','Remove')); } else { OC.Settings.Apps.rebuildNavigation(); @@ -722,9 +722,9 @@ OC.Settings.Apps = OC.Settings.Apps || { OC.dialogs.info( t( 'settings', - 'The app has been enabled but needs to be updated. You will be redirected to the update page in 5 seconds.' + 'The app has been enabled but needs to be upgraded. You will be redirected to the upgrade page in 5 seconds.' ), - t('settings','App update'), + t('settings','App upgrade'), function () { window.location.reload(); }, diff --git a/settings/js/settings/personalInfo.js b/settings/js/settings/personalInfo.js index 3a4542df748..0a39e607762 100644 --- a/settings/js/settings/personalInfo.js +++ b/settings/js/settings/personalInfo.js @@ -60,7 +60,7 @@ function updateAvatar (hidedefault) { $displaydiv.avatar(user.uid, 145, true, null, function() { $displaydiv.removeClass('loading'); $('#displayavatar img').show(); - if($('#displayavatar img').length === 0) { + if($('#displayavatar img').length === 0 || oc_userconfig.avatar.generated) { $('#removeavatar').removeClass('inlineblock').addClass('hidden'); } else { $('#removeavatar').removeClass('hidden').addClass('inlineblock'); @@ -129,6 +129,7 @@ function avatarResponseHandler (data) { $warning.hide(); if (data.status === "success") { $('#displayavatar .avatardiv').removeClass('icon-loading'); + oc_userconfig.avatar.generated = false; updateAvatar(); } else if (data.data === "notsquare") { showAvatarCropper(); @@ -256,8 +257,14 @@ $(document).ready(function () { }); + var userSettings = new OC.Settings.UserSettings(); var federationSettingsView = new OC.Settings.FederationSettingsView({ - el: '#personal-settings' + el: '#personal-settings', + config: userSettings + }); + + userSettings.on("sync", function() { + updateAvatar(false); }); federationSettingsView.render(); @@ -362,6 +369,7 @@ $(document).ready(function () { type: 'DELETE', url: OC.generateUrl('/avatar/'), success: function () { + oc_userconfig.avatar.generated = true; updateAvatar(true); } }); @@ -392,7 +400,7 @@ $(document).ready(function () { // Load the big avatar var user = OC.getCurrentUser(); $('#avatarform .avatardiv').avatar(user.uid, 145, true, null, function() { - if($('#displayavatar img').length === 0) { + if($('#displayavatar img').length === 0 || oc_userconfig.avatar.generated) { $('#removeavatar').removeClass('inlineblock').addClass('hidden'); } else { $('#removeavatar').removeClass('hidden').addClass('inlineblock'); diff --git a/tests/Core/Command/Maintenance/UpdateTheme.php b/tests/Core/Command/Maintenance/UpdateTheme.php index fbdea0b72b4..cbc417dbdba 100644 --- a/tests/Core/Command/Maintenance/UpdateTheme.php +++ b/tests/Core/Command/Maintenance/UpdateTheme.php @@ -74,7 +74,7 @@ class UpdateThemeTest extends TestCase { ->method('clear') ->with(''); $this->cacheFactory->expects($this->once()) - ->method('create') + ->method('createDistributed') ->with('imagePath') ->willReturn($cache); self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); diff --git a/tests/Core/Controller/LoginControllerTest.php b/tests/Core/Controller/LoginControllerTest.php index e02b8403a2a..ddf7a865d66 100644 --- a/tests/Core/Controller/LoginControllerTest.php +++ b/tests/Core/Controller/LoginControllerTest.php @@ -182,12 +182,43 @@ class LoginControllerTest extends TestCase { 'alt_login' => [], 'rememberLoginState' => 0, 'resetPasswordLink' => null, + 'hideRemeberLoginState' => false, ], 'guest' ); $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('', '', '')); } + public function testShowLoginFormForFlowAuth() { + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(false); + $this->session + ->expects($this->once()) + ->method('exists') + ->with('client.flow.state.token') + ->willReturn(true); + + $expectedResponse = new TemplateResponse( + 'core', + 'login', + [ + 'messages' => [], + 'redirect_url' => 'login/flow', + 'loginName' => '', + 'user_autofocus' => true, + 'canResetPassword' => true, + 'alt_login' => [], + 'rememberLoginState' => 0, + 'resetPasswordLink' => null, + 'hideRemeberLoginState' => true, + ], + 'guest' + ); + $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('', 'login/flow', '')); + } + /** * @return array */ @@ -240,6 +271,7 @@ class LoginControllerTest extends TestCase { 'alt_login' => [], 'rememberLoginState' => 0, 'resetPasswordLink' => false, + 'hideRemeberLoginState' => false, ], 'guest' ); @@ -278,6 +310,7 @@ class LoginControllerTest extends TestCase { 'alt_login' => [], 'rememberLoginState' => 0, 'resetPasswordLink' => false, + 'hideRemeberLoginState' => false, ], 'guest' ); diff --git a/tests/Settings/Activity/SecurityProviderTest.php b/tests/Settings/Activity/SecurityProviderTest.php index 21fc28f3c3b..552548984d7 100644 --- a/tests/Settings/Activity/SecurityProviderTest.php +++ b/tests/Settings/Activity/SecurityProviderTest.php @@ -87,7 +87,7 @@ class SecurityProviderTest extends TestCase { ->willReturn('security'); $this->l10n->expects($this->once()) ->method('get') - ->with('core', $lang) + ->with('settings', $lang) ->willReturn($l); $this->urlGenerator->expects($this->once()) ->method('imagePath') @@ -119,7 +119,7 @@ class SecurityProviderTest extends TestCase { ->willReturn('security'); $this->l10n->expects($this->once()) ->method('get') - ->with('core', $lang) + ->with('settings', $lang) ->willReturn($l); $event->expects($this->once()) ->method('getSubject') diff --git a/tests/acceptance/features/app-files.feature b/tests/acceptance/features/app-files.feature index ac2d05fac2c..2cb43611b9a 100644 --- a/tests/acceptance/features/app-files.feature +++ b/tests/acceptance/features/app-files.feature @@ -23,6 +23,17 @@ Feature: app-files When I open the details view for "welcome.txt" Then I see that the details view for "All files" section is open + Scenario: open the menu in a public shared link + Given I act as John + And I am logged in + And I share the link for "welcome.txt" + And I write down the shared link + When I act as Jane + And I visit the shared link I wrote down + And I see that the current page is the shared link I wrote down + And I open the Share menu + Then I see that the Share menu is shown + Scenario: set a password to a shared link Given I am logged in And I share the link for "welcome.txt" diff --git a/tests/acceptance/features/bootstrap/FilesAppContext.php b/tests/acceptance/features/bootstrap/FilesAppContext.php index 338823a9478..4951dc43f1d 100644 --- a/tests/acceptance/features/bootstrap/FilesAppContext.php +++ b/tests/acceptance/features/bootstrap/FilesAppContext.php @@ -452,7 +452,16 @@ class FilesAppContext implements Context, ActorAwareInterface { * @Given I write down the shared link */ public function iWriteDownTheSharedLink() { - $this->actor->getSharedNotebook()["shared link"] = $this->actor->find(self::shareLinkField(), 10)->getValue(); + // The shared link field always exists in the DOM (once the "Sharing" + // tab is loaded), but its value is the actual shared link only when it + // is visible. + if (!$this->waitForElementToBeEventuallyShown( + self::shareLinkField(), + $timeout = 10 * $this->actor->getFindTimeoutMultiplier())) { + PHPUnit_Framework_Assert::fail("The shared link was not shown yet after $timeout seconds"); + } + + $this->actor->getSharedNotebook()["shared link"] = $this->actor->find(self::shareLinkField())->getValue(); } /** @@ -606,7 +615,9 @@ class FilesAppContext implements Context, ActorAwareInterface { * @When I see that the :tabName tab in the details view is eventually loaded */ public function iSeeThatTheTabInTheDetailsViewIsEventuallyLoaded($tabName) { - if (!$this->waitForElementToBeEventuallyNotShown(self::loadingIconForTabInCurrentSectionDetailsViewNamed($tabName), $timeout = 10)) { + if (!$this->waitForElementToBeEventuallyNotShown( + self::loadingIconForTabInCurrentSectionDetailsViewNamed($tabName), + $timeout = 10 * $this->actor->getFindTimeoutMultiplier())) { PHPUnit_Framework_Assert::fail("The $tabName tab in the details view has not been loaded after $timeout seconds"); } } @@ -622,7 +633,9 @@ class FilesAppContext implements Context, ActorAwareInterface { * @Then I see that the working icon for password protect is eventually not shown */ public function iSeeThatTheWorkingIconForPasswordProtectIsEventuallyNotShown() { - if (!$this->waitForElementToBeEventuallyNotShown(self::passwordProtectWorkingIcon(), $timeout = 10)) { + if (!$this->waitForElementToBeEventuallyNotShown( + self::passwordProtectWorkingIcon(), + $timeout = 10 * $this->actor->getFindTimeoutMultiplier())) { PHPUnit_Framework_Assert::fail("The working icon for password protect is still shown after $timeout seconds"); } } @@ -637,10 +650,24 @@ class FilesAppContext implements Context, ActorAwareInterface { $this->iSeeThatTheWorkingIconForPasswordProtectIsEventuallyNotShown(); } + private function waitForElementToBeEventuallyShown($elementLocator, $timeout = 10, $timeoutStep = 1) { + $actor = $this->actor; + + $elementShownCallback = function() use ($actor, $elementLocator) { + try { + return $actor->find($elementLocator)->isVisible(); + } catch (NoSuchElementException $exception) { + return false; + } + }; + + return Utils::waitFor($elementShownCallback, $timeout, $timeoutStep); + } + private function waitForElementToBeEventuallyNotShown($elementLocator, $timeout = 10, $timeoutStep = 1) { $actor = $this->actor; - $elementNotFoundCallback = function() use ($actor, $elementLocator) { + $elementNotShownCallback = function() use ($actor, $elementLocator) { try { return !$actor->find($elementLocator)->isVisible(); } catch (NoSuchElementException $exception) { @@ -648,6 +675,6 @@ class FilesAppContext implements Context, ActorAwareInterface { } }; - return Utils::waitFor($elementNotFoundCallback, $timeout, $timeoutStep); + return Utils::waitFor($elementNotShownCallback, $timeout, $timeoutStep); } } diff --git a/tests/acceptance/features/bootstrap/FilesSharingAppContext.php b/tests/acceptance/features/bootstrap/FilesSharingAppContext.php index 88c1180c753..f3386b46db9 100644 --- a/tests/acceptance/features/bootstrap/FilesSharingAppContext.php +++ b/tests/acceptance/features/bootstrap/FilesSharingAppContext.php @@ -54,6 +54,49 @@ class FilesSharingAppContext implements Context, ActorAwareInterface { /** * @return Locator */ + public static function shareMenuButton() { + return Locator::forThe()->id("share-menutoggle")-> + describedAs("Share menu button in Shared file page"); + } + + /** + * @return Locator + */ + public static function shareMenu() { + return Locator::forThe()->id("share-menu")-> + describedAs("Share menu in Shared file page"); + } + + /** + * @return Locator + */ + public static function downloadItemInShareMenu() { + return Locator::forThe()->id("download")-> + descendantOf(self::shareMenu())-> + describedAs("Download item in Share menu in Shared file page"); + } + + /** + * @return Locator + */ + public static function directLinkItemInShareMenu() { + return Locator::forThe()->id("directLink-container")-> + descendantOf(self::shareMenu())-> + describedAs("Direct link item in Share menu in Shared file page"); + } + + /** + * @return Locator + */ + public static function saveItemInShareMenu() { + return Locator::forThe()->id("save")-> + descendantOf(self::shareMenu())-> + describedAs("Save item in Share menu in Shared file page"); + } + + /** + * @return Locator + */ public static function textPreview() { return Locator::forThe()->css(".text-preview")-> describedAs("Text preview in Shared file page"); @@ -75,6 +118,13 @@ class FilesSharingAppContext implements Context, ActorAwareInterface { } /** + * @When I open the Share menu + */ + public function iOpenTheShareMenu() { + $this->actor->find(self::shareMenuButton(), 10)->click(); + } + + /** * @Then I see that the current page is the Authenticate page for the shared link I wrote down */ public function iSeeThatTheCurrentPageIsTheAuthenticatePageForTheSharedLinkIWroteDown() { @@ -101,10 +151,44 @@ class FilesSharingAppContext implements Context, ActorAwareInterface { } /** + * @Then I see that the Share menu is shown + */ + public function iSeeThatTheShareMenuIsShown() { + // Unlike other menus, the Share menu is always present in the DOM, so + // the element could be found when it was no made visible yet due to the + // command not having been processed by the browser. + if (!$this->waitForElementToBeEventuallyShown( + self::shareMenu(), $timeout = 10 * $this->actor->getFindTimeoutMultiplier())) { + PHPUnit_Framework_Assert::fail("The Share menu is not visible yet after $timeout seconds"); + } + + PHPUnit_Framework_Assert::assertTrue( + $this->actor->find(self::downloadItemInShareMenu())->isVisible()); + PHPUnit_Framework_Assert::assertTrue( + $this->actor->find(self::directLinkItemInShareMenu())->isVisible()); + PHPUnit_Framework_Assert::assertTrue( + $this->actor->find(self::saveItemInShareMenu())->isVisible()); + } + + /** * @Then I see that the shared file preview shows the text :text */ public function iSeeThatTheSharedFilePreviewShowsTheText($text) { PHPUnit_Framework_Assert::assertContains($text, $this->actor->find(self::textPreview(), 10)->getText()); } + private function waitForElementToBeEventuallyShown($elementLocator, $timeout = 10, $timeoutStep = 1) { + $actor = $this->actor; + + $elementShownCallback = function() use ($actor, $elementLocator) { + try { + return $actor->find($elementLocator)->isVisible(); + } catch (NoSuchElementException $exception) { + return false; + } + }; + + return Utils::waitFor($elementShownCallback, $timeout, $timeoutStep); + } + } diff --git a/tests/lib/App/AppManagerTest.php b/tests/lib/App/AppManagerTest.php index c2c0ea55072..c361db7b76b 100644 --- a/tests/lib/App/AppManagerTest.php +++ b/tests/lib/App/AppManagerTest.php @@ -100,7 +100,7 @@ class AppManagerTest extends TestCase { $this->cache = $this->createMock(ICache::class); $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); $this->cacheFactory->expects($this->any()) - ->method('create') + ->method('createDistributed') ->with('settings') ->willReturn($this->cache); $this->manager = new AppManager($this->userSession, $this->appConfig, $this->groupManager, $this->cacheFactory, $this->eventDispatcher); diff --git a/tests/lib/AvatarTest.php b/tests/lib/AvatarTest.php index 240aecc115e..9da719c26de 100644 --- a/tests/lib/AvatarTest.php +++ b/tests/lib/AvatarTest.php @@ -210,7 +210,7 @@ class AvatarTest extends \Test\TestCase { ->method('putContent') ->with($image->data()); - $this->config->expects($this->once()) + $this->config->expects($this->exactly(3)) ->method('setUserValue'); $this->config->expects($this->once()) ->method('getUserValue'); diff --git a/tests/lib/Collaboration/Collaborators/MailPluginTest.php b/tests/lib/Collaboration/Collaborators/MailPluginTest.php index 9c9d9cff909..b728ae521e2 100644 --- a/tests/lib/Collaboration/Collaborators/MailPluginTest.php +++ b/tests/lib/Collaboration/Collaborators/MailPluginTest.php @@ -31,6 +31,8 @@ use OCP\Collaboration\Collaborators\SearchResultType; use OCP\Contacts\IManager; use OCP\Federation\ICloudIdManager; use OCP\IConfig; +use OCP\IGroupManager; +use OCP\IUserSession; use OCP\Share; use Test\TestCase; @@ -50,17 +52,25 @@ class MailPluginTest extends TestCase { /** @var SearchResult */ protected $searchResult; + /** @var IGroupManager|\PHPUnit_Framework_MockObject_MockObject */ + protected $groupManager; + + /** @var IUserSession|\PHPUnit_Framework_MockObject_MockObject */ + protected $userSession; + public function setUp() { parent::setUp(); $this->config = $this->createMock(IConfig::class); $this->contactsManager = $this->createMock(IManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->userSession = $this->createMock(IUserSession::class); $this->cloudIdManager = new CloudIdManager(); $this->searchResult = new SearchResult(); } public function instantiatePlugin() { - $this->plugin = new MailPlugin($this->contactsManager, $this->cloudIdManager, $this->config); + $this->plugin = new MailPlugin($this->contactsManager, $this->cloudIdManager, $this->config, $this->groupManager, $this->userSession); } /** @@ -333,4 +343,131 @@ class MailPluginTest extends TestCase { ] ]; } + + /** + * @dataProvider dataGetEmailGroupsOnly + * + * @param string $searchTerm + * @param array $contacts + * @param array $expected + * @param bool $exactIdMatch + * @param bool $reachedEnd + * @param array groups + */ + public function testSearchGroupsOnly($searchTerm, $contacts, $expected, $exactIdMatch, $reachedEnd, $userToGroupMapping) { + $this->config->expects($this->any()) + ->method('getAppValue') + ->willReturnCallback( + function($appName, $key, $default) { + if ($appName === 'core' && $key === 'shareapi_allow_share_dialog_user_enumeration') { + return 'yes'; + } else if ($appName === 'core' && $key === 'shareapi_only_share_with_group_members') { + return 'yes'; + } + return $default; + } + ); + + $this->instantiatePlugin(); + + /** @var \OCP\IUser | \PHPUnit_Framework_MockObject_MockObject */ + $currentUser = $this->createMock('\OCP\IUser'); + + $currentUser->expects($this->any()) + ->method('getUID') + ->willReturn('currentUser'); + + $this->contactsManager->expects($this->any()) + ->method('search') + ->with($searchTerm, ['EMAIL', 'FN']) + ->willReturn($contacts); + + $this->userSession->expects($this->any()) + ->method('getUser') + ->willReturn($currentUser); + + $this->groupManager->expects($this->any()) + ->method('getUserGroupIds') + ->willReturnCallback(function(\OCP\IUser $user) use ($userToGroupMapping) { + return $userToGroupMapping[$user->getUID()]; + }); + + $this->groupManager->expects($this->any()) + ->method('isInGroup') + ->willReturnCallback(function($userId, $group) use ($userToGroupMapping) { + return in_array($group, $userToGroupMapping[$userId]); + }); + + $moreResults = $this->plugin->search($searchTerm, 0, 0, $this->searchResult); + $result = $this->searchResult->asArray(); + + $this->assertSame($exactIdMatch, $this->searchResult->hasExactIdMatch(new SearchResultType('emails'))); + $this->assertEquals($expected, $result); + $this->assertSame($reachedEnd, $moreResults); + } + + public function dataGetEmailGroupsOnly() { + return [ + // The user `User` can share with the current user + [ + 'test', + [ + [ + 'FN' => 'User', + 'EMAIL' => ['test@example.com'], + 'CLOUD' => ['test@localhost'], + 'isLocalSystemBook' => true, + 'UID' => 'User' + ] + ], + ['users' => [['label' => 'User (test@example.com)','value' => ['shareType' => 0, 'shareWith' => 'test'],]], 'emails' => [], 'exact' => ['emails' => [], 'users' => []]], + false, + true, + [ + "currentUser" => ["group1"], + "User" => ["group1"] + ] + ], + // The user `User` cannot share with the current user + [ + 'test', + [ + [ + 'FN' => 'User', + 'EMAIL' => ['test@example.com'], + 'CLOUD' => ['test@localhost'], + 'isLocalSystemBook' => true, + 'UID' => 'User' + ] + ], + ['emails'=> [], 'exact' => ['emails' => []]], + false, + true, + [ + "currentUser" => ["group1"], + "User" => ["group2"] + ] + ], + // The user `User` cannot share with the current user, but there is an exact match on the e-mail address -> share by e-mail + [ + 'test@example.com', + [ + [ + 'FN' => 'User', + 'EMAIL' => ['test@example.com'], + 'CLOUD' => ['test@localhost'], + 'isLocalSystemBook' => true, + 'UID' => 'User' + ] + ], + ['emails' => [], 'exact' => ['emails' => [['label' => 'test@example.com', 'value' => ['shareType' => 4,'shareWith' => 'test@example.com']]]]], + false, + true, + [ + "currentUser" => ["group1"], + "User" => ["group2"] + ] + ] + ]; + } } diff --git a/tests/lib/IntegrityCheck/CheckerTest.php b/tests/lib/IntegrityCheck/CheckerTest.php index 049017cb5e8..09e6990a0f3 100644 --- a/tests/lib/IntegrityCheck/CheckerTest.php +++ b/tests/lib/IntegrityCheck/CheckerTest.php @@ -60,7 +60,7 @@ class CheckerTest extends TestCase { $this->cacheFactory ->expects($this->any()) - ->method('create') + ->method('createDistributed') ->with('oc.integritycheck.checker') ->will($this->returnValue(new NullCache())); diff --git a/tests/lib/LegacyHelperTest.php b/tests/lib/LegacyHelperTest.php index f1e22ea600e..736c5bf7fad 100644 --- a/tests/lib/LegacyHelperTest.php +++ b/tests/lib/LegacyHelperTest.php @@ -12,6 +12,17 @@ use OC\Files\View; use OC_Helper; class LegacyHelperTest extends \Test\TestCase { + /** @var string */ + private $originalWebRoot; + + public function setUp() { + $this->originalWebRoot = \OC::$WEBROOT; + } + + public function tearDown() { + // Reset webRoot + \OC::$WEBROOT = $this->originalWebRoot; + } /** * @dataProvider humanFileSizeProvider diff --git a/tests/lib/Security/RateLimiting/Backend/MemoryCacheTest.php b/tests/lib/Security/RateLimiting/Backend/MemoryCacheTest.php index 34c326e72e1..bacd2b7bf6f 100644 --- a/tests/lib/Security/RateLimiting/Backend/MemoryCacheTest.php +++ b/tests/lib/Security/RateLimiting/Backend/MemoryCacheTest.php @@ -46,7 +46,7 @@ class MemoryCacheTest extends TestCase { $this->cacheFactory ->expects($this->once()) - ->method('create') + ->method('createDistributed') ->with('OC\Security\RateLimiting\Backend\MemoryCache') ->willReturn($this->cache); diff --git a/tests/lib/Template/SCSSCacherTest.php b/tests/lib/Template/SCSSCacherTest.php index 3825bc44c59..fca9500810e 100644 --- a/tests/lib/Template/SCSSCacherTest.php +++ b/tests/lib/Template/SCSSCacherTest.php @@ -352,19 +352,10 @@ class SCSSCacherTest extends \Test\TestCase { } public function testRebaseUrls() { - $webDir = 'apps/files/css'; + $webDir = '/apps/files/css'; $css = '#id { background-image: url(\'../img/image.jpg\'); }'; $actual = self::invokePrivate($this->scssCacher, 'rebaseUrls', [$css, $webDir]); - $expected = '#id { background-image: url(\'../../../apps/files/css/../img/image.jpg\'); }'; - $this->assertEquals($expected, $actual); - } - - public function testRebaseUrlsIgnoreFrontendController() { - $this->config->expects($this->once())->method('getSystemValue')->with('htaccess.IgnoreFrontController', false)->willReturn(true); - $webDir = 'apps/files/css'; - $css = '#id { background-image: url(\'../img/image.jpg\'); }'; - $actual = self::invokePrivate($this->scssCacher, 'rebaseUrls', [$css, $webDir]); - $expected = '#id { background-image: url(\'../../apps/files/css/../img/image.jpg\'); }'; + $expected = '#id { background-image: url(\'/apps/files/css/../img/image.jpg\'); }'; $this->assertEquals($expected, $actual); } @@ -393,4 +384,55 @@ class SCSSCacherTest extends \Test\TestCase { $this->assertEquals(substr($result, 1), $actual); } + private function randomString() { + return sha1(uniqid(mt_rand(), true)); + } + + private function rrmdir($directory) { + $files = array_diff(scandir($directory), array('.','..')); + foreach ($files as $file) { + if (is_dir($directory . '/' . $file)) { + $this->rrmdir($directory . '/' . $file); + } else { + unlink($directory . '/' . $file); + } + } + return rmdir($directory); + } + + public function dataGetWebDir() { + return [ + // Root installation + ['/http/core/css', 'core', '', '/http', '/core/css'], + ['/http/apps/scss/css', 'scss', '', '/http', '/apps/scss/css'], + ['/srv/apps2/scss/css', 'scss', '', '/http', '/apps2/scss/css'], + // Sub directory install + ['/http/nextcloud/core/css', 'core', '/nextcloud', '/http/nextcloud', '/nextcloud/core/css'], + ['/http/nextcloud/apps/scss/css', 'scss', '/nextcloud', '/http/nextcloud', '/nextcloud/apps/scss/css'], + ['/srv/apps2/scss/css', 'scss', '/nextcloud', '/http/nextcloud', '/apps2/scss/css'] + ]; + } + + /** + * @param $path + * @param $appName + * @param $webRoot + * @param $serverRoot + * @dataProvider dataGetWebDir + */ + public function testgetWebDir($path, $appName, $webRoot, $serverRoot, $correctWebDir) { + $tmpDir = sys_get_temp_dir().'/'.$this->randomString(); + // Adding fake apps folder and create fake app install + \OC::$APPSROOTS[] = [ + 'path' => $tmpDir.'/srv/apps2', + 'url' => '/apps2', + 'writable' => false + ]; + mkdir($tmpDir.$path, 0777, true); + $actual = self::invokePrivate($this->scssCacher, 'getWebDir', [$tmpDir.$path, $appName, $tmpDir.$serverRoot, $webRoot]); + $this->assertEquals($correctWebDir, $actual); + array_pop(\OC::$APPSROOTS); + $this->rrmdir($tmpDir.$path); + } + } diff --git a/tests/lib/Updater/VersionCheckTest.php b/tests/lib/Updater/VersionCheckTest.php index ff04aa17681..89a335722d7 100644 --- a/tests/lib/Updater/VersionCheckTest.php +++ b/tests/lib/Updater/VersionCheckTest.php @@ -161,7 +161,7 @@ class VersionCheckTest extends \Test\TestCase { $this->config ->expects($this->at(6)) ->method('setAppValue') - ->with('core', 'lastupdateResult', 'false'); + ->with('core', 'lastupdateResult', '[]'); $updateXml = 'Invalid XML Response!'; $this->updater @@ -265,4 +265,55 @@ class VersionCheckTest extends \Test\TestCase { $this->assertSame($expectedResult, $this->updater->check()); } + + public function testCheckWithMissingAttributeXmlResponse() { + $expectedResult = [ + 'version' => '', + 'versionstring' => '', + 'url' => '', + 'web' => '', + 'autoupdater' => '', + ]; + + $this->config + ->expects($this->at(0)) + ->method('getAppValue') + ->with('core', 'lastupdatedat') + ->will($this->returnValue(0)); + $this->config + ->expects($this->at(1)) + ->method('getSystemValue') + ->with('updater.server.url', 'https://updates.nextcloud.com/updater_server/') + ->willReturnArgument(1); + $this->config + ->expects($this->at(2)) + ->method('setAppValue') + ->with('core', 'lastupdatedat', $this->isType('integer')); + $this->config + ->expects($this->at(4)) + ->method('getAppValue') + ->with('core', 'installedat') + ->will($this->returnValue('installedat')); + $this->config + ->expects($this->at(5)) + ->method('getAppValue') + ->with('core', 'lastupdatedat') + ->will($this->returnValue('lastupdatedat')); + + // missing autoupdater element should still not fail + $updateXml = '<?xml version="1.0"?> +<owncloud> + <version></version> + <versionstring></versionstring> + <url></url> + <web></web> +</owncloud>'; + $this->updater + ->expects($this->once()) + ->method('getUrlContent') + ->with($this->buildUpdateUrl('https://updates.nextcloud.com/updater_server/')) + ->will($this->returnValue($updateXml)); + + $this->assertSame($expectedResult, $this->updater->check()); + } } diff --git a/tests/lib/UrlGeneratorTest.php b/tests/lib/UrlGeneratorTest.php index 69067f51e08..340c9c7082d 100644 --- a/tests/lib/UrlGeneratorTest.php +++ b/tests/lib/UrlGeneratorTest.php @@ -27,6 +27,8 @@ class UrlGeneratorTest extends \Test\TestCase { private $request; /** @var IURLGenerator */ private $urlGenerator; + /** @var string */ + private $originalWebRoot; public function setUp() { parent::setUp(); @@ -38,6 +40,12 @@ class UrlGeneratorTest extends \Test\TestCase { $this->cacheFactory, $this->request ); + $this->originalWebRoot = \OC::$WEBROOT; + } + + public function tearDown() { + // Reset webRoot + \OC::$WEBROOT = $this->originalWebRoot; } private function mockBaseUrl() { @@ -47,7 +55,6 @@ class UrlGeneratorTest extends \Test\TestCase { $this->request->expects($this->once()) ->method('getServerHost') ->willReturn('localhost'); - } /** @@ -156,4 +163,3 @@ class UrlGeneratorTest extends \Test\TestCase { } } - |