diff options
35 files changed, 910 insertions, 97 deletions
diff --git a/.gitignore b/.gitignore index a6baffc6a59..531e372e607 100644 --- a/.gitignore +++ b/.gitignore @@ -115,3 +115,4 @@ Vagrantfile /tests/data/testimage-copy.png /config/config-autotest-backup.php /config/autoconfig.php +clover.xml diff --git a/.travis.yml b/.travis.yml index 74dbfe00f01..95e8a2f7de4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ php: env: global: - - APP=dav + - TEST_DAV=$(tests/travis/changed_app.sh dav) - TC=litmus-v2 matrix: - DB=sqlite @@ -21,20 +21,22 @@ addons: - realpath before_install: - - bash tests/travis/test_for_app.sh $APP - - bash tests/travis/before_install.sh $DB + - sh -c "if [ '$TEST_DAV' = '1' ]; then bash tests/travis/before_install.sh $DB; fi" install: - - bash tests/travis/install.sh $DB + - sh -c "if [ '$TEST_DAV' = '1' ]; then bash tests/travis/install.sh $DB; fi" script: - - bash apps/$APP/tests/travis/$TC.sh + - sh -c "if [ '$TEST_DAV' != '1' ]; then echo \"Not testing DAV\"; fi" + - sh -c "if [ '$TEST_DAV' = '1' ]; then echo \"Testing DAV\"; fi" + + - sh -c "if [ '$TEST_DAV' = '1' ]; then bash apps/dav/tests/travis/$TC.sh; fi" matrix: include: - php: 5.4 - env: DB=pgsql;TC=litmus-v1;APP=dav + env: DB=pgsql;TC=litmus-v1 # - php: 5.4 -# env: DB=mysql;TC=caldavtester;APP=dav +# env: DB=mysql;TC=caldavtester fast_finish: true diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index c84d6c3c47d..99804b1b9be 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -767,7 +767,7 @@ */ elementToFile: function($el){ $el = $($el); - return { + var data = { id: parseInt($el.attr('data-id'), 10), name: $el.attr('data-file'), mimetype: $el.attr('data-mime'), @@ -777,6 +777,15 @@ etag: $el.attr('data-etag'), permissions: parseInt($el.attr('data-permissions'), 10) }; + var icon = $el.attr('data-icon'); + if (icon) { + data.icon = icon; + } + var mountType = $el.attr('data-mounttype'); + if (mountType) { + data.mountType = mountType; + } + return data; }, /** @@ -899,11 +908,12 @@ mtime = parseInt(fileData.mtime, 10), mime = fileData.mimetype, path = fileData.path, + dataIcon = null, linkUrl; options = options || {}; if (isNaN(mtime)) { - mtime = new Date().getTime() + mtime = new Date().getTime(); } if (type === 'dir') { @@ -911,6 +921,7 @@ if (fileData.mountType && fileData.mountType.indexOf('external') === 0) { icon = OC.MimeType.getIconUrl('dir-external'); + dataIcon = icon; } } @@ -926,6 +937,11 @@ "data-permissions": fileData.permissions || this.getDirectoryPermissions() }); + if (dataIcon) { + // icon override + tr.attr('data-icon', dataIcon); + } + if (fileData.mountType) { tr.attr('data-mounttype', fileData.mountType); } @@ -1177,7 +1193,7 @@ // display actions this.fileActions.display(filenameTd, !options.silent, this); - if (fileData.isPreviewAvailable) { + if (fileData.isPreviewAvailable && mime !== 'httpd/unix-directory') { var iconDiv = filenameTd.find('.thumbnail'); // lazy load / newly inserted td ? // the typeof check ensures that the default value of animate is true diff --git a/apps/files/js/mainfileinfodetailview.js b/apps/files/js/mainfileinfodetailview.js index abf7da52ff4..69c796e492f 100644 --- a/apps/files/js/mainfileinfodetailview.js +++ b/apps/files/js/mainfileinfodetailview.js @@ -128,8 +128,8 @@ $iconDiv.addClass('icon-loading icon-32'); this.loadPreview(this.model.getFullPath(), this.model.get('mimetype'), this.model.get('etag'), $iconDiv, $container, this.model.isImage()); } else { - // TODO: special icons / shared / external - $iconDiv.css('background-image', 'url("' + OC.MimeType.getIconUrl('dir') + '")'); + var iconUrl = this.model.get('icon') || OC.MimeType.getIconUrl('dir'); + $iconDiv.css('background-image', 'url("' + iconUrl + '")'); OC.Util.scaleFixForIE8($iconDiv); } this.$el.find('[title]').tooltip({placement: 'bottom'}); diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index 994e1d32844..05e6fcc6122 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -1166,7 +1166,7 @@ describe('OCA.Files.FileList tests', function() { it('renders provided icon for file when provided', function() { var fileData = { type: 'file', - name: 'test dir', + name: 'test file', icon: OC.webroot + '/core/img/filetypes/application-pdf.svg', mimetype: 'application/pdf' }; @@ -1178,7 +1178,7 @@ describe('OCA.Files.FileList tests', function() { it('renders preview when no icon was provided and preview is available', function() { var fileData = { type: 'file', - name: 'test dir', + name: 'test file', isPreviewAvailable: true }; var $tr = fileList.add(fileData); @@ -1192,7 +1192,7 @@ describe('OCA.Files.FileList tests', function() { it('renders default file type icon when no icon was provided and no preview is available', function() { var fileData = { type: 'file', - name: 'test dir', + name: 'test file', isPreviewAvailable: false }; var $tr = fileList.add(fileData); @@ -1200,6 +1200,47 @@ describe('OCA.Files.FileList tests', function() { expect(OC.TestUtil.getImageUrl($imgDiv)).toEqual(OC.webroot + '/core/img/filetypes/file.svg'); expect(previewLoadStub.notCalled).toEqual(true); }); + it('does not render preview for directories', function() { + var fileData = { + type: 'dir', + mimetype: 'httpd/unix-directory', + name: 'test dir', + isPreviewAvailable: true + }; + var $tr = fileList.add(fileData); + var $td = $tr.find('td.filename'); + expect(OC.TestUtil.getImageUrl($td.find('.thumbnail'))).toEqual(OC.webroot + '/core/img/filetypes/folder.svg'); + expect(previewLoadStub.notCalled).toEqual(true); + }); + it('render external storage icon for external storage root', function() { + var fileData = { + type: 'dir', + mimetype: 'httpd/unix-directory', + name: 'test dir', + isPreviewAvailable: true, + mountType: 'external-root' + }; + var $tr = fileList.add(fileData); + var $td = $tr.find('td.filename'); + expect(OC.TestUtil.getImageUrl($td.find('.thumbnail'))).toEqual(OC.webroot + '/core/img/filetypes/folder-external.svg'); + expect(previewLoadStub.notCalled).toEqual(true); + }); + it('render external storage icon for external storage subdir', function() { + var fileData = { + type: 'dir', + mimetype: 'httpd/unix-directory', + name: 'test dir', + isPreviewAvailable: true, + mountType: 'external' + }; + var $tr = fileList.add(fileData); + var $td = $tr.find('td.filename'); + expect(OC.TestUtil.getImageUrl($td.find('.thumbnail'))).toEqual(OC.webroot + '/core/img/filetypes/folder-external.svg'); + expect(previewLoadStub.notCalled).toEqual(true); + // default icon override + expect($tr.attr('data-icon')).toEqual(OC.webroot + '/core/img/filetypes/folder-external.svg'); + }); + }); describe('viewer mode', function() { it('enabling viewer mode hides files table and action buttons', function() { diff --git a/apps/files/tests/js/mainfileinfodetailviewSpec.js b/apps/files/tests/js/mainfileinfodetailviewSpec.js index f4403196f2e..460629806c8 100644 --- a/apps/files/tests/js/mainfileinfodetailviewSpec.js +++ b/apps/files/tests/js/mainfileinfodetailviewSpec.js @@ -112,6 +112,20 @@ describe('OCA.Files.MainFileInfoDetailView tests', function() { lazyLoadPreviewStub.restore(); }); + it('uses icon from model if present in model', function() { + var lazyLoadPreviewStub = sinon.stub(fileList, 'lazyLoadPreview'); + testFileInfo.set('mimetype', 'httpd/unix-directory'); + testFileInfo.set('icon', OC.MimeType.getIconUrl('dir-external')); + view.setFileInfo(testFileInfo); + + expect(lazyLoadPreviewStub.notCalled).toEqual(true); + + expect(view.$el.find('.thumbnail').hasClass('icon-loading')).toEqual(false); + expect(view.$el.find('.thumbnail').css('background-image')) + .toContain('filetypes/folder-external.svg'); + + lazyLoadPreviewStub.restore(); + }); it('displays thumbnail', function() { var lazyLoadPreviewStub = sinon.stub(fileList, 'lazyLoadPreview'); diff --git a/apps/files_external/controller/storagescontroller.php b/apps/files_external/controller/storagescontroller.php index f754565f628..048f3588ed7 100644 --- a/apps/files_external/controller/storagescontroller.php +++ b/apps/files_external/controller/storagescontroller.php @@ -237,9 +237,21 @@ abstract class StoragesController extends Controller { ) ); } catch (InsufficientDataForMeaningfulAnswerException $e) { - $storage->setStatus(\OC_Mount_Config::STATUS_INDETERMINATE); + $storage->setStatus( + \OC_Mount_Config::STATUS_INDETERMINATE, + $this->l10n->t('Insufficient data: %s', [$e->getMessage()]) + ); } catch (StorageNotAvailableException $e) { - $storage->setStatus(\OC_Mount_Config::STATUS_ERROR); + $storage->setStatus( + \OC_Mount_Config::STATUS_ERROR, + $e->getMessage() + ); + } catch (\Exception $e) { + // FIXME: convert storage exceptions to StorageNotAvailableException + $storage->setStatus( + \OC_Mount_Config::STATUS_ERROR, + get_class($e).': '.$e->getMessage() + ); } } diff --git a/apps/files_external/js/settings.js b/apps/files_external/js/settings.js index 5da34c52193..a839f396b9b 100644 --- a/apps/files_external/js/settings.js +++ b/apps/files_external/js/settings.js @@ -643,6 +643,10 @@ MountConfigListView.prototype = _.extend({ }); addSelect2(this.$el.find('tr:not(#addMountPoint) .applicableUsers'), this._userListLimit); + this.$el.tooltip({ + selector: '.status span', + container: 'body' + }); this._initEvents(); @@ -709,11 +713,12 @@ MountConfigListView.prototype = _.extend({ } highlightInput($target); var $tr = $target.closest('tr'); + this.updateStatus($tr, null); var timer = $tr.data('save-timer'); clearTimeout(timer); timer = setTimeout(function() { - self.saveStorageConfig($tr); + self.saveStorageConfig($tr, null, timer); }, 2000); $tr.data('save-timer', timer); }, @@ -931,8 +936,9 @@ MountConfigListView.prototype = _.extend({ * * @param $tr storage row * @param Function callback callback to call after save + * @param concurrentTimer only update if the timer matches this */ - saveStorageConfig:function($tr, callback) { + saveStorageConfig:function($tr, callback, concurrentTimer) { var self = this; var storage = this.getStorageConfig($tr); if (!storage.validate()) { @@ -942,15 +948,23 @@ MountConfigListView.prototype = _.extend({ this.updateStatus($tr, StorageConfig.Status.IN_PROGRESS); storage.save({ success: function(result) { - self.updateStatus($tr, result.status); - $tr.attr('data-id', result.id); - - if (_.isFunction(callback)) { - callback(storage); + if (concurrentTimer === undefined + || $tr.data('save-timer') === concurrentTimer + ) { + self.updateStatus($tr, result.status, result.statusMessage); + $tr.attr('data-id', result.id); + + if (_.isFunction(callback)) { + callback(storage); + } } }, error: function() { - self.updateStatus($tr, StorageConfig.Status.ERROR); + if (concurrentTimer === undefined + || $tr.data('save-timer') === concurrentTimer + ) { + self.updateStatus($tr, StorageConfig.Status.ERROR); + } } }); }, @@ -971,7 +985,7 @@ MountConfigListView.prototype = _.extend({ this.updateStatus($tr, StorageConfig.Status.IN_PROGRESS); storage.recheck({ success: function(result) { - self.updateStatus($tr, result.status); + self.updateStatus($tr, result.status, result.statusMessage); }, error: function() { self.updateStatus($tr, StorageConfig.Status.ERROR); @@ -984,11 +998,15 @@ MountConfigListView.prototype = _.extend({ * * @param {jQuery} $tr * @param {int} status + * @param {string} message */ - updateStatus: function($tr, status) { + updateStatus: function($tr, status, message) { var $statusSpan = $tr.find('.status span'); $statusSpan.removeClass('loading-small success indeterminate error'); switch (status) { + case null: + // remove status + break; case StorageConfig.Status.IN_PROGRESS: $statusSpan.addClass('loading-small'); break; @@ -1001,6 +1019,7 @@ MountConfigListView.prototype = _.extend({ default: $statusSpan.addClass('error'); } + $statusSpan.attr('data-original-title', (typeof message === 'string') ? message : ''); }, /** diff --git a/apps/files_external/lib/config.php b/apps/files_external/lib/config.php index d9fdb748fcd..56a7e547ec6 100644 --- a/apps/files_external/lib/config.php +++ b/apps/files_external/lib/config.php @@ -269,6 +269,7 @@ class OC_Mount_Config { } } catch (Exception $exception) { \OCP\Util::logException('files_external', $exception); + throw $e; } } return self::STATUS_ERROR; diff --git a/apps/files_external/lib/storageconfig.php b/apps/files_external/lib/storageconfig.php index 70aaa186783..86a7e6ffa12 100644 --- a/apps/files_external/lib/storageconfig.php +++ b/apps/files_external/lib/storageconfig.php @@ -73,6 +73,13 @@ class StorageConfig implements \JsonSerializable { private $status; /** + * Status message + * + * @var string + */ + private $statusMessage; + + /** * Priority * * @var int @@ -295,7 +302,7 @@ class StorageConfig implements \JsonSerializable { } /** - * Sets the storage status, whether the config worked last time + * Gets the storage status, whether the config worked last time * * @return int $status status */ @@ -304,12 +311,23 @@ class StorageConfig implements \JsonSerializable { } /** + * Gets the message describing the storage status + * + * @return string|null + */ + public function getStatusMessage() { + return $this->statusMessage; + } + + /** * Sets the storage status, whether the config worked last time * * @param int $status status + * @param string|null $message optional message */ - public function setStatus($status) { + public function setStatus($status, $message = null) { $this->status = $status; + $this->statusMessage = $message; } /** @@ -341,6 +359,9 @@ class StorageConfig implements \JsonSerializable { if (!is_null($this->status)) { $result['status'] = $this->status; } + if (!is_null($this->statusMessage)) { + $result['statusMessage'] = $this->statusMessage; + } return $result; } } diff --git a/apps/files_external/lib/swift.php b/apps/files_external/lib/swift.php index beb47ce0bce..e946e7feb77 100644 --- a/apps/files_external/lib/swift.php +++ b/apps/files_external/lib/swift.php @@ -106,7 +106,10 @@ class Swift extends \OC\Files\Storage\Common { $this->getContainer()->getPartialObject($path); return true; } catch (ClientErrorResponseException $e) { - \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); + // Expected response is "404 Not Found", so only log if it isn't + if ($e->getResponse()->getStatusCode() !== 404) { + \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); + } return false; } } diff --git a/apps/files_sharing/js/share.js b/apps/files_sharing/js/share.js index 30a803f3207..63225a0d8ec 100644 --- a/apps/files_sharing/js/share.js +++ b/apps/files_sharing/js/share.js @@ -130,6 +130,13 @@ // remove icon, if applicable OC.Share.markFileAsShared($tr, false, false); } + var newIcon = $tr.attr('data-icon'); + // in case markFileAsShared decided to change the icon, + // we need to modify the model + // (FIXME: yes, this is hacky) + if (fileInfoModel.get('icon') !== newIcon) { + fileInfoModel.set('icon', newIcon); + } }); fileList.registerTabView(shareTab); }, diff --git a/apps/provisioning_api/appinfo/app.php b/apps/provisioning_api/appinfo/app.php deleted file mode 100644 index 40d8d5d04d3..00000000000 --- a/apps/provisioning_api/appinfo/app.php +++ /dev/null @@ -1,20 +0,0 @@ -<?php -/** - - * - * @copyright Copyright (c) 2015, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ diff --git a/core/ajax/share.php b/core/ajax/share.php index 68afd7b4e84..fd42a94de6e 100644 --- a/core/ajax/share.php +++ b/core/ajax/share.php @@ -360,8 +360,8 @@ if (isset($_POST['action']) && isset($_POST['itemType']) && isset($_POST['itemSo } if ((!isset($_GET['itemShares']) - || !is_array((string)$_GET['itemShares'][OCP\Share::SHARE_TYPE_USER]) - || !in_array($uid, (string)$_GET['itemShares'][OCP\Share::SHARE_TYPE_USER])) + || !is_array($_GET['itemShares'][OCP\Share::SHARE_TYPE_USER]) + || !in_array($uid, $_GET['itemShares'][OCP\Share::SHARE_TYPE_USER])) && $uid != OC_User::getUser()) { $shareWith[] = array( 'label' => $displayName, @@ -386,8 +386,8 @@ if (isset($_POST['action']) && isset($_POST['itemType']) && isset($_POST['itemSo if ($count < $request_limit) { if (!isset($_GET['itemShares']) || !isset($_GET['itemShares'][OCP\Share::SHARE_TYPE_GROUP]) - || !is_array((string)$_GET['itemShares'][OCP\Share::SHARE_TYPE_GROUP]) - || !in_array($group, (string)$_GET['itemShares'][OCP\Share::SHARE_TYPE_GROUP])) { + || !is_array($_GET['itemShares'][OCP\Share::SHARE_TYPE_GROUP]) + || !in_array($group, $_GET['itemShares'][OCP\Share::SHARE_TYPE_GROUP])) { $shareWith[] = array( 'label' => $group, 'value' => array( diff --git a/core/command/maintenance/repair.php b/core/command/maintenance/repair.php index 5df362f8111..f7c0cc46048 100644 --- a/core/command/maintenance/repair.php +++ b/core/command/maintenance/repair.php @@ -26,6 +26,7 @@ namespace OC\Core\Command\Maintenance; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Repair extends Command { @@ -49,10 +50,24 @@ class Repair extends Command { protected function configure() { $this ->setName('maintenance:repair') - ->setDescription('repair this installation'); + ->setDescription('repair this installation') + ->addOption( + 'include-expensive', + null, + InputOption::VALUE_NONE, + 'Use this option when you want to include resource and load expensive tasks' + ) + ; } protected function execute(InputInterface $input, OutputInterface $output) { + $includeExpensive = $input->getOption('include-expensive'); + if ($includeExpensive) { + foreach ($this->repair->getExpensiveRepairSteps() as $step) { + $this->repair->addStep($step); + } + } + $maintenanceMode = $this->config->getSystemValue('maintenance', false); $this->config->setSystemValue('maintenance', true); diff --git a/core/js/mimetype.js b/core/js/mimetype.js index b0de8eb8411..3cc33ce2830 100644 --- a/core/js/mimetype.js +++ b/core/js/mimetype.js @@ -46,6 +46,8 @@ OC.MimeType = { return 'folder'; } else if (mimeType === 'dir-shared' && $.inArray('folder-shared', files) !== -1) { return 'folder-shared'; + } else if (mimeType === 'dir-public' && $.inArray('folder-public', files) !== -1) { + return 'folder-public'; } else if (mimeType === 'dir-external' && $.inArray('folder-external', files) !== -1) { return 'folder-external'; } else if ($.inArray(icon, files) !== -1) { diff --git a/core/js/share.js b/core/js/share.js index 1131ae8f112..e14e19a2543 100644 --- a/core/js/share.js +++ b/core/js/share.js @@ -253,14 +253,25 @@ OC.Share = _.extend(OC.Share || {}, { // update folder icon if (type === 'dir' && (hasShares || hasLink || owner)) { if (hasLink) { - shareFolderIcon = OC.imagePath('core', 'filetypes/folder-public'); + shareFolderIcon = OC.MimeType.getIconUrl('dir-public'); } else { - shareFolderIcon = OC.imagePath('core', 'filetypes/folder-shared'); + shareFolderIcon = OC.MimeType.getIconUrl('dir-shared'); } $tr.find('.filename .thumbnail').css('background-image', 'url(' + shareFolderIcon + ')'); + $tr.attr('data-icon', shareFolderIcon); } else if (type === 'dir') { - shareFolderIcon = OC.imagePath('core', 'filetypes/folder'); + var mountType = $tr.attr('data-mounttype'); + // FIXME: duplicate of FileList._createRow logic for external folder, + // need to refactor the icon logic into a single code path eventually + if (mountType && mountType.indexOf('external') === 0) { + shareFolderIcon = OC.MimeType.getIconUrl('dir-external'); + $tr.attr('data-icon', shareFolderIcon); + } else { + shareFolderIcon = OC.MimeType.getIconUrl('dir'); + // back to default + $tr.removeAttr('data-icon'); + } $tr.find('.filename .thumbnail').css('background-image', 'url(' + shareFolderIcon + ')'); } // update share action text / icon diff --git a/core/js/tests/specs/shareSpec.js b/core/js/tests/specs/shareSpec.js index 9e80f4fe19d..5c76ea600b8 100644 --- a/core/js/tests/specs/shareSpec.js +++ b/core/js/tests/specs/shareSpec.js @@ -142,6 +142,13 @@ describe('OC.Share tests', function() { checkIcon('filetypes/folder-public'); }); + it('shows external storage icon if external mount point', function() { + $file.attr('data-type', 'dir'); + $file.attr('data-mountType', 'external'); + OC.Share.markFileAsShared($file, false, false); + + checkIcon('filetypes/folder-external'); + }); }); describe('displaying the recipoients', function() { diff --git a/core/js/tests/specs/sharedialogshareelistview.js b/core/js/tests/specs/sharedialogshareelistview.js new file mode 100644 index 00000000000..d468ce790dc --- /dev/null +++ b/core/js/tests/specs/sharedialogshareelistview.js @@ -0,0 +1,158 @@ +/** + * ownCloud + * + * @author Tom Needham + * @copyright 2015 Tom Needham <tom@owncloud.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 library. If not, see <http://www.gnu.org/licenses/>. + * + */ + +/* global oc_appconfig */ +describe('OC.Share.ShareDialogShareeListView', function () { + + var oldCurrentUser; + var fileInfoModel; + var configModel; + var shareModel; + var listView; + var setPermissionsStub; + + beforeEach(function () { + /* jshint camelcase:false */ + oldAppConfig = _.extend({}, oc_appconfig.core); + oc_appconfig.core.enforcePasswordForPublicLink = false; + + $('#testArea').append('<input id="mailPublicNotificationEnabled" name="mailPublicNotificationEnabled" type="hidden" value="yes">'); + + fileInfoModel = new OCA.Files.FileInfoModel({ + id: 123, + name: 'shared_file_name.txt', + path: '/subdir', + size: 100, + mimetype: 'text/plain', + permissions: 31, + sharePermissions: 31 + }); + + var attributes = { + itemType: fileInfoModel.isDirectory() ? 'folder' : 'file', + itemSource: fileInfoModel.get('id'), + possiblePermissions: 31, + permissions: 31 + }; + + shareModel = new OC.Share.ShareItemModel(attributes, { + configModel: configModel, + fileInfoModel: fileInfoModel + }); + + configModel = new OC.Share.ShareConfigModel({ + enforcePasswordForPublicLink: false, + isResharingAllowed: true, + enforcePasswordForPublicLink: false, + isDefaultExpireDateEnabled: false, + isDefaultExpireDateEnforced: false, + defaultExpireDate: 7 + }); + + listView = new OC.Share.ShareDialogShareeListView({ + configModel: configModel, + model: shareModel + }); + + // required for proper event propagation when simulating clicks in some cases (jquery bugs) + $('#testArea').append(listView.$el); + + shareModel.set({ + linkShare: {isLinkShare: false} + }); + + oldCurrentUser = OC.currentUser; + OC.currentUser = 'user0'; + setPermissionsStub = sinon.stub(listView.model, 'setPermissions'); + }); + + afterEach(function () { + OC.currentUser = oldCurrentUser; + /* jshint camelcase:false */ + oc_appconfig.core = oldAppConfig; + listView.remove(); + setPermissionsStub.restore(); + }); + + describe('Manages checkbox events correctly', function () { + it('Checks cruds boxes when edit box checked', function () { + shareModel.set('shares', [{ + id: 100, + item_source: 123, + permissions: 1, + share_type: OC.Share.SHARE_TYPE_USER, + share_with: 'user1', + share_with_displayname: 'User One' + }]); + listView.render(); + listView.$el.find("input[name='edit']").click(); + expect(listView.$el.find("input[name='update']").is(':checked')).toEqual(true); + expect(setPermissionsStub.called).toEqual(true); + }); + + it('Checks edit box when create/update/delete are checked', function () { + shareModel.set('shares', [{ + id: 100, + item_source: 123, + permissions: 1, + share_type: OC.Share.SHARE_TYPE_USER, + share_with: 'user1', + share_with_displayname: 'User One' + }]); + listView.render(); + listView.$el.find("input[name='update']").click(); + expect(listView.$el.find("input[name='edit']").is(':checked')).toEqual(true); + expect(setPermissionsStub.called).toEqual(true); + }); + + it('shows cruds checkboxes when toggled', function () { + shareModel.set('shares', [{ + id: 100, + item_source: 123, + permissions: 1, + share_type: OC.Share.SHARE_TYPE_USER, + share_with: 'user1', + share_with_displayname: 'User One' + }]); + listView.render(); + listView.$el.find('a.showCruds').click(); + expect(listView.$el.find('li.cruds').hasClass('hidden')).toEqual(false); + }); + + it('sends notification to user when checkbox clicked', function () { + shareModel.set('shares', [{ + id: 100, + item_source: 123, + permissions: 1, + share_type: OC.Share.SHARE_TYPE_USER, + share_with: 'user1', + share_with_displayname: 'User One' + }]); + listView.render(); + var notificationStub = sinon.stub(listView.model, 'sendNotificationForShare'); + listView.$el.find("input[name='mailNotification']").click(); + expect(notificationStub.called).toEqual(true); + notificationStub.restore(); + }); + + }); + +}); diff --git a/core/templates/internalaltmail.php b/core/templates/internalaltmail.php new file mode 100644 index 00000000000..38531d109b7 --- /dev/null +++ b/core/templates/internalaltmail.php @@ -0,0 +1,13 @@ +<?php +print_unescaped($l->t("Hey there,\n\njust letting you know that %s shared %s with you.\nView it: %s\n\n", array($_['user_displayname'], $_['filename'], $_['link']))); +if ( isset($_['expiration']) ) { + print_unescaped($l->t("The share will expire on %s.", array($_['expiration']))); + print_unescaped("\n\n"); +} +// TRANSLATORS term at the end of a mail +p($l->t("Cheers!")); +?> + +-- +<?php p($theme->getName() . ' - ' . $theme->getSlogan()); ?> +<?php print_unescaped("\n".$theme->getBaseUrl()); diff --git a/core/templates/internalmail.php b/core/templates/internalmail.php new file mode 100644 index 00000000000..0e73a601857 --- /dev/null +++ b/core/templates/internalmail.php @@ -0,0 +1,39 @@ +<table cellspacing="0" cellpadding="0" border="0" width="100%"> +<tr><td> +<table cellspacing="0" cellpadding="0" border="0" width="600px"> +<tr> +<td bgcolor="<?php p($theme->getMailHeaderColor());?>" width="20px"> </td> +<td bgcolor="<?php p($theme->getMailHeaderColor());?>"> +<img src="<?php p(OC_Helper::makeURLAbsolute(image_path('', 'logo-mail.gif'))); ?>" alt="<?php p($theme->getName()); ?>"/> +</td> +</tr> +<tr><td colspan="2"> </td></tr> +<tr> +<td width="20px"> </td> +<td style="font-weight:normal; font-size:0.8em; line-height:1.2em; font-family:verdana,'arial',sans;"> +<?php +print_unescaped($l->t('Hey there,<br><br>just letting you know that %s shared <strong>%s</strong> with you.<br><a href="%s">View it!</a><br><br>', array($_['user_displayname'], $_['filename'], $_['link']))); +if ( isset($_['expiration']) ) { + p($l->t("The share will expire on %s.", array($_['expiration']))); + print_unescaped('<br><br>'); +} +// TRANSLATORS term at the end of a mail +p($l->t('Cheers!')); +?> +</td> +</tr> +<tr><td colspan="2"> </td></tr> +<tr> +<td width="20px"> </td> +<td style="font-weight:normal; font-size:0.8em; line-height:1.2em; font-family:verdana,'arial',sans;">--<br> +<?php p($theme->getName()); ?> - +<?php p($theme->getSlogan()); ?> +<br><a href="<?php p($theme->getBaseUrl()); ?>"><?php p($theme->getBaseUrl());?></a> +</td> +</tr> +<tr> +<td colspan="2"> </td> +</tr> +</table> +</td></tr> +</table> diff --git a/lib/private/files/cache/cache.php b/lib/private/files/cache/cache.php index f3e22701f40..71720ac58bf 100644 --- a/lib/private/files/cache/cache.php +++ b/lib/private/files/cache/cache.php @@ -313,6 +313,14 @@ class Cache { $fields = array( 'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted', 'etag', 'permissions'); + + $doNotCopyStorageMTime = false; + if (array_key_exists('mtime', $data) && $data['mtime'] === null) { + // this horrific magic tells it to not copy storage_mtime to mtime + unset($data['mtime']); + $doNotCopyStorageMTime = true; + } + $params = array(); $queryParts = array(); foreach ($data as $name => $value) { @@ -325,7 +333,7 @@ class Cache { $queryParts[] = '`mimepart`'; $value = $this->mimetypeLoader->getId($value); } elseif ($name === 'storage_mtime') { - if (!isset($data['mtime'])) { + if (!$doNotCopyStorageMTime && !isset($data['mtime'])) { $params[] = $value; $queryParts[] = '`mtime`'; } @@ -477,6 +485,9 @@ class Cache { list($sourceStorageId, $sourcePath) = $sourceCache->getMoveInfo($sourcePath); list($targetStorageId, $targetPath) = $this->getMoveInfo($targetPath); + // sql for final update + $moveSql = 'UPDATE `*PREFIX*filecache` SET `storage` = ?, `path` = ?, `path_hash` = ?, `name` = ?, `parent` =? WHERE `fileid` = ?'; + if ($sourceData['mimetype'] === 'httpd/unix-directory') { //find all child entries $sql = 'SELECT `path`, `fileid` FROM `*PREFIX*filecache` WHERE `storage` = ? AND `path` LIKE ?'; @@ -490,11 +501,12 @@ class Cache { $newTargetPath = $targetPath . substr($child['path'], $sourceLength); \OC_DB::executeAudited($query, [$targetStorageId, $newTargetPath, md5($newTargetPath), $child['fileid']]); } + \OC_DB::executeAudited($moveSql, [$targetStorageId, $targetPath, md5($targetPath), basename($targetPath), $newParentId, $sourceId]); \OC_DB::commit(); + } else { + \OC_DB::executeAudited($moveSql, [$targetStorageId, $targetPath, md5($targetPath), basename($targetPath), $newParentId, $sourceId]); } - $sql = 'UPDATE `*PREFIX*filecache` SET `storage` = ?, `path` = ?, `path_hash` = ?, `name` = ?, `parent` =? WHERE `fileid` = ?'; - \OC_DB::executeAudited($sql, [$targetStorageId, $targetPath, md5($targetPath), basename($targetPath), $newParentId, $sourceId]); } /** diff --git a/lib/private/files/cache/updater.php b/lib/private/files/cache/updater.php index 4cc5f511565..85dcc8589de 100644 --- a/lib/private/files/cache/updater.php +++ b/lib/private/files/cache/updater.php @@ -194,12 +194,26 @@ class Updater { $targetCache->correctFolderSize($targetInternalPath); $this->correctParentStorageMtime($sourceStorage, $sourceInternalPath); $this->correctParentStorageMtime($targetStorage, $targetInternalPath); + $this->updateStorageMTimeOnly($targetStorage, $targetInternalPath); $this->propagator->addChange($source); $this->propagator->addChange($target); $this->propagator->propagateChanges(); } } + private function updateStorageMTimeOnly($storage, $internalPath) { + $cache = $storage->getCache(); + $fileId = $cache->getId($internalPath); + if ($fileId !== -1) { + $cache->update( + $fileId, [ + 'mtime' => null, // this magic tells it to not overwrite mtime + 'storage_mtime' => $storage->filemtime($internalPath) + ] + ); + } + } + /** * update the storage_mtime of the direct parent in the cache to the mtime from the storage * diff --git a/lib/private/files/storage/wrapper/availability.php b/lib/private/files/storage/wrapper/availability.php index 37319a8f7d1..d6ce78f6e44 100644 --- a/lib/private/files/storage/wrapper/availability.php +++ b/lib/private/files/storage/wrapper/availability.php @@ -220,6 +220,9 @@ class Availability extends Wrapper { /** {@inheritdoc} */ public function file_exists($path) { + if ($path === '') { + return true; + } $this->checkAvailability(); try { return parent::file_exists($path); diff --git a/lib/private/repair.php b/lib/private/repair.php index 20219e313fd..f6ac7ebe65b 100644 --- a/lib/private/repair.php +++ b/lib/private/repair.php @@ -34,6 +34,7 @@ use OC\Repair\AssetCache; use OC\Repair\CleanTags; use OC\Repair\Collation; use OC\Repair\DropOldJobs; +use OC\Repair\OldGroupMembershipShares; use OC\Repair\RemoveGetETagEntries; use OC\Repair\SqliteAutoincrement; use OC\Repair\DropOldTables; @@ -119,6 +120,18 @@ class Repair extends BasicEmitter { } /** + * Returns expensive repair steps to be run on the + * command line with a special option. + * + * @return array of RepairStep instances + */ + public static function getExpensiveRepairSteps() { + return [ + new OldGroupMembershipShares(\OC::$server->getDatabaseConnection(), \OC::$server->getGroupManager()), + ]; + } + + /** * Returns the repair steps to be run before an * upgrade. * diff --git a/lib/private/share/mailnotifications.php b/lib/private/share/mailnotifications.php index 8056260bf17..2797e5ed99b 100644 --- a/lib/private/share/mailnotifications.php +++ b/lib/private/share/mailnotifications.php @@ -136,7 +136,7 @@ class MailNotifications { $link = \OCP\Util::linkToAbsolute('files', 'index.php', $args); - list($htmlBody, $textBody) = $this->createMailBody($filename, $link, $expiration); + list($htmlBody, $textBody) = $this->createMailBody($filename, $link, $expiration, 'internal'); // send it out now try { @@ -210,20 +210,20 @@ class MailNotifications { * @param string $filename the shared file * @param string $link link to the shared file * @param int $expiration expiration date (timestamp) + * @param bool $prefix prefix of mail template files * @return array an array of the html mail body and the plain text mail body */ - private function createMailBody($filename, $link, $expiration) { - + private function createMailBody($filename, $link, $expiration, $prefix = '') { $formattedDate = $expiration ? $this->l->l('date', $expiration) : null; - $html = new \OC_Template("core", "mail", ""); + $html = new \OC_Template('core', $prefix . 'mail', ''); $html->assign ('link', $link); $html->assign ('user_displayname', $this->senderDisplayName); $html->assign ('filename', $filename); $html->assign('expiration', $formattedDate); $htmlMail = $html->fetchPage(); - $plainText = new \OC_Template("core", "altmail", ""); + $plainText = new \OC_Template('core', $prefix . 'altmail', ''); $plainText->assign ('link', $link); $plainText->assign ('user_displayname', $this->senderDisplayName); $plainText->assign ('filename', $filename); diff --git a/lib/repair/oldgroupmembershipshares.php b/lib/repair/oldgroupmembershipshares.php new file mode 100644 index 00000000000..2d701ac9fb7 --- /dev/null +++ b/lib/repair/oldgroupmembershipshares.php @@ -0,0 +1,117 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Repair; + + +use OC\Hooks\BasicEmitter; +use OC\RepairStep; +use OCP\IDBConnection; +use OCP\IGroupManager; +use OCP\Share; + +class OldGroupMembershipShares extends BasicEmitter implements RepairStep { + + /** @var \OCP\IDBConnection */ + protected $connection; + + /** @var \OCP\IGroupManager */ + protected $groupManager; + + /** + * @var array [gid => [uid => (bool)]] + */ + protected $memberships; + + /** + * @param IDBConnection $connection + * @param IGroupManager $groupManager + */ + public function __construct(IDBConnection $connection, IGroupManager $groupManager) { + $this->connection = $connection; + $this->groupManager = $groupManager; + } + + /** + * Returns the step's name + * + * @return string + */ + public function getName() { + return 'Remove shares of old group memberships'; + } + + /** + * Run repair step. + * Must throw exception on error. + * + * @throws \Exception in case of failure + */ + public function run() { + $deletedEntries = 0; + + $query = $this->connection->getQueryBuilder(); + $query->select(['s1.id', $query->createFunction('s1.`share_with` AS `user`'), $query->createFunction('s2.`share_with` AS `group`')]) + ->from('share', 's1') + ->where($query->expr()->isNotNull('s1.parent')) + // \OC\Share\Constant::$shareTypeGroupUserUnique === 2 + ->andWhere($query->expr()->eq('s1.share_type', $query->expr()->literal(2))) + ->andWhere($query->expr()->isNotNull('s2.id')) + ->andWhere($query->expr()->eq('s2.share_type', $query->expr()->literal(Share::SHARE_TYPE_GROUP))) + ->leftJoin('s1', 'share', 's2', $query->expr()->eq('s1.parent', 's2.id')); + + $deleteQuery = $this->connection->getQueryBuilder(); + $deleteQuery->delete('share') + ->where($query->expr()->eq('id', $deleteQuery->createParameter('share'))); + + $result = $query->execute(); + while ($row = $result->fetch()) { + if (!$this->isMember($row['group'], $row['user'])) { + $deletedEntries += $deleteQuery->setParameter('share', (int) $row['id']) + ->execute(); + } + } + $result->closeCursor(); + + if ($deletedEntries) { + $this->emit('\OC\Repair', 'info', array('Removed ' . $deletedEntries . ' shares where user is not a member of the group anymore')); + } + } + + /** + * @param string $gid + * @param string $uid + * @return bool + */ + protected function isMember($gid, $uid) { + if (isset($this->memberships[$gid][$uid])) { + return $this->memberships[$gid][$uid]; + } + + $isMember = $this->groupManager->isInGroup($uid, $gid); + if (!isset($this->memberships[$gid])) { + $this->memberships[$gid] = []; + } + $this->memberships[$gid][$uid] = $isMember; + + return $isMember; + } +} diff --git a/lib/repair/repairinvalidshares.php b/lib/repair/repairinvalidshares.php index 4b0aeb70c12..5a4cb445ce9 100644 --- a/lib/repair/repairinvalidshares.php +++ b/lib/repair/repairinvalidshares.php @@ -70,11 +70,43 @@ class RepairInvalidShares extends BasicEmitter implements \OC\RepairStep { } } + /** + * Remove shares where the parent share does not exist anymore + */ + private function removeSharesNonExistingParent() { + $deletedEntries = 0; + + $query = $this->connection->getQueryBuilder(); + $query->select('s1.parent') + ->from('share', 's1') + ->where($query->expr()->isNotNull('s1.parent')) + ->andWhere($query->expr()->isNull('s2.id')) + ->leftJoin('s1', 'share', 's2', $query->expr()->eq('s1.parent', 's2.id')) + ->groupBy('s1.parent'); + + $deleteQuery = $this->connection->getQueryBuilder(); + $deleteQuery->delete('share') + ->where($query->expr()->eq('parent', $deleteQuery->createParameter('parent'))); + + $result = $query->execute(); + while ($row = $result->fetch()) { + $deletedEntries += $deleteQuery->setParameter('parent', (int) $row['parent']) + ->execute(); + } + $result->closeCursor(); + + if ($deletedEntries) { + $this->emit('\OC\Repair', 'info', array('Removed ' . $deletedEntries . ' shares where the parent did not exist')); + } + } + public function run() { $ocVersionFromBeforeUpdate = $this->config->getSystemValue('version', '0.0.0'); if (version_compare($ocVersionFromBeforeUpdate, '8.2.0.7', '<')) { // this situation was only possible before 8.2 $this->removeExpirationDateFromNonLinkShares(); } + + $this->removeSharesNonExistingParent(); } } diff --git a/settings/templates/users/main.php b/settings/templates/users/main.php index 0abe31f4a59..f50f83b38b3 100644 --- a/settings/templates/users/main.php +++ b/settings/templates/users/main.php @@ -46,35 +46,35 @@ translation('settings'); <div id="userlistoptions"> <p> <input type="checkbox" name="StorageLocation" value="StorageLocation" id="CheckboxStorageLocation" - <?php if ($_['show_storage_location'] === 'true') print_unescaped('checked="checked"'); ?> /> + class="checkbox" <?php if ($_['show_storage_location'] === 'true') print_unescaped('checked="checked"'); ?> /> <label for="CheckboxStorageLocation"> <?php p($l->t('Show storage location')) ?> </label> </p> <p> <input type="checkbox" name="LastLogin" value="LastLogin" id="CheckboxLastLogin" - <?php if ($_['show_last_login'] === 'true') print_unescaped('checked="checked"'); ?> /> + class="checkbox" <?php if ($_['show_last_login'] === 'true') print_unescaped('checked="checked"'); ?> /> <label for="CheckboxLastLogin"> <?php p($l->t('Show last log in')) ?> </label> </p> <p> <input type="checkbox" name="UserBackend" value="UserBackend" id="CheckboxUserBackend" - <?php if ($_['show_backend'] === 'true') print_unescaped('checked="checked"'); ?> /> + class="checkbox" <?php if ($_['show_backend'] === 'true') print_unescaped('checked="checked"'); ?> /> <label for="CheckboxUserBackend"> <?php p($l->t('Show user backend')) ?> </label> </p> <p> <input type="checkbox" name="MailOnUserCreate" value="MailOnUserCreate" id="CheckboxMailOnUserCreate" - <?php if ($_['send_email'] === 'true') print_unescaped('checked="checked"'); ?> /> + class="checkbox" <?php if ($_['send_email'] === 'true') print_unescaped('checked="checked"'); ?> /> <label for="CheckboxMailOnUserCreate"> <?php p($l->t('Send email to new user')) ?> </label> </p> <p> <input type="checkbox" name="EmailAddress" value="EmailAddress" id="CheckboxEmailAddress" - <?php if ($_['show_email'] === 'true') print_unescaped('checked="checked"'); ?> /> + class="checkbox" <?php if ($_['show_email'] === 'true') print_unescaped('checked="checked"'); ?> /> <label for="CheckboxEmailAddress"> <?php p($l->t('Show email address')) ?> </label> diff --git a/tests/lib/files/cache/updater.php b/tests/lib/files/cache/updater.php index e3fa26829b4..b7e76aeace4 100644 --- a/tests/lib/files/cache/updater.php +++ b/tests/lib/files/cache/updater.php @@ -161,6 +161,47 @@ class Updater extends \Test\TestCase { $this->assertEquals($cached['fileid'], $cachedTarget['fileid']); } + public function testUpdateStorageMTime() { + $this->storage->mkdir('sub'); + $this->storage->mkdir('sub2'); + $this->storage->file_put_contents('sub/foo.txt', 'qwerty'); + + $this->updater->update('sub'); + $this->updater->update('sub/foo.txt'); + $this->updater->update('sub2'); + + $cachedSourceParent = $this->cache->get('sub'); + $cachedSource = $this->cache->get('sub/foo.txt'); + + $this->storage->rename('sub/foo.txt', 'sub2/bar.txt'); + + // simulate storage having a different mtime + $testmtime = 1433323578; + + // source storage mtime change + $this->storage->touch('sub', $testmtime); + + // target storage mtime change + $this->storage->touch('sub2', $testmtime); + // some storages (like Dropbox) change storage mtime on rename + $this->storage->touch('sub2/bar.txt', $testmtime); + + $this->updater->rename('sub/foo.txt', 'sub2/bar.txt'); + + $cachedTargetParent = $this->cache->get('sub2'); + $cachedTarget = $this->cache->get('sub2/bar.txt'); + + $this->assertEquals($cachedSource['mtime'], $cachedTarget['mtime'], 'file mtime preserved'); + + $this->assertNotEquals($cachedTarget['storage_mtime'], $cachedTarget['mtime'], 'mtime is not storage_mtime for moved file'); + + $this->assertEquals($testmtime, $cachedTarget['storage_mtime'], 'target file storage_mtime propagated'); + $this->assertNotEquals($testmtime, $cachedTarget['mtime'], 'target file mtime changed, not from storage'); + + $this->assertEquals($testmtime, $cachedTargetParent['storage_mtime'], 'target parent storage_mtime propagated'); + $this->assertNotEquals($testmtime, $cachedTargetParent['mtime'], 'target folder mtime changed, not from storage'); + } + public function testNewFileDisabled() { $this->storage->file_put_contents('foo.txt', 'bar'); $this->assertFalse($this->cache->inCache('foo.txt')); diff --git a/tests/lib/files/view.php b/tests/lib/files/view.php index a84b8badd5a..a7979146b85 100644 --- a/tests/lib/files/view.php +++ b/tests/lib/files/view.php @@ -1976,9 +1976,13 @@ class View extends \Test\TestCase { $view = new \OC\Files\View('/' . $this->user . '/files/'); $storage = $this->getMockBuilder('\OC\Files\Storage\Temporary') - ->setMethods([$operation]) + ->setMethods([$operation, 'filemtime']) ->getMock(); + $storage->expects($this->any()) + ->method('filemtime') + ->will($this->returnValue(123456789)); + $sourcePath = 'original.txt'; $targetPath = 'target.txt'; @@ -2117,9 +2121,13 @@ class View extends \Test\TestCase { ->setMethods([$storageOperation]) ->getMock(); $storage2 = $this->getMockBuilder('\OC\Files\Storage\Temporary') - ->setMethods([$storageOperation]) + ->setMethods([$storageOperation, 'filemtime']) ->getMock(); + $storage2->expects($this->any()) + ->method('filemtime') + ->will($this->returnValue(123456789)); + $sourcePath = 'original.txt'; $targetPath = 'substorage/target.txt'; diff --git a/tests/lib/repair/oldgroupmembershipsharestest.php b/tests/lib/repair/oldgroupmembershipsharestest.php new file mode 100644 index 00000000000..74f68bd7899 --- /dev/null +++ b/tests/lib/repair/oldgroupmembershipsharestest.php @@ -0,0 +1,138 @@ +<?php +/** + * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace Test\Repair; + +use OC\Repair\OldGroupMembershipShares; +use OC\Share\Constants; + +class OldGroupMembershipSharesTest extends \Test\TestCase { + + /** @var OldGroupMembershipShares */ + protected $repair; + + /** @var \OCP\IDBConnection */ + protected $connection; + + /** @var \OCP\IGroupManager|\PHPUnit_Framework_MockObject_MockObject */ + protected $groupManager; + + protected function setUp() { + parent::setUp(); + + /** \OCP\IGroupManager|\PHPUnit_Framework_MockObject_MockObject */ + $this->groupManager = $this->getMockBuilder('OCP\IGroupManager') + ->disableOriginalConstructor() + ->getMock(); + $this->connection = \OC::$server->getDatabaseConnection(); + + $this->deleteAllShares(); + } + + protected function tearDown() { + $this->deleteAllShares(); + + parent::tearDown(); + } + + protected function deleteAllShares() { + $qb = $this->connection->getQueryBuilder(); + $qb->delete('share')->execute(); + } + + public function testRun() { + $repair = new OldGroupMembershipShares( + $this->connection, + $this->groupManager + ); + + $this->groupManager->expects($this->exactly(2)) + ->method('isInGroup') + ->willReturnMap([ + ['member', 'group', true], + ['not-a-member', 'group', false], + ]); + + $parent = $this->createShare(Constants::SHARE_TYPE_GROUP, 'group', null); + $group2 = $this->createShare(Constants::SHARE_TYPE_GROUP, 'group2', $parent); + $user1 = $this->createShare(Constants::SHARE_TYPE_USER, 'user1', $parent); + + // \OC\Share\Constant::$shareTypeGroupUserUnique === 2 + $member = $this->createShare(2, 'member', $parent); + $notAMember = $this->createShare(2, 'not-a-member', $parent); + + $query = $this->connection->getQueryBuilder(); + $result = $query->select('id') + ->from('share') + ->orderBy('id', 'ASC') + ->execute(); + $rows = $result->fetchAll(); + $this->assertSame([['id' => $parent], ['id' => $group2], ['id' => $user1], ['id' => $member], ['id' => $notAMember]], $rows); + $result->closeCursor(); + + $repair->run(); + + $query = $this->connection->getQueryBuilder(); + $result = $query->select('id') + ->from('share') + ->orderBy('id', 'ASC') + ->execute(); + $rows = $result->fetchAll(); + $this->assertSame([['id' => $parent], ['id' => $group2], ['id' => $user1], ['id' => $member]], $rows); + $result->closeCursor(); + } + + /** + * @param string $shareType + * @param string $shareWith + * @param null|int $parent + * @return int + */ + protected function createShare($shareType, $shareWith, $parent) { + $qb = $this->connection->getQueryBuilder(); + $shareValues = [ + 'share_type' => $qb->expr()->literal($shareType), + 'share_with' => $qb->expr()->literal($shareWith), + 'uid_owner' => $qb->expr()->literal('user1'), + 'item_type' => $qb->expr()->literal('folder'), + 'item_source' => $qb->expr()->literal(123), + 'item_target' => $qb->expr()->literal('/123'), + 'file_source' => $qb->expr()->literal(123), + 'file_target' => $qb->expr()->literal('/test'), + 'permissions' => $qb->expr()->literal(1), + 'stime' => $qb->expr()->literal(time()), + 'expiration' => $qb->expr()->literal('2015-09-25 00:00:00'), + ]; + + if ($parent) { + $shareValues['parent'] = $qb->expr()->literal($parent); + } + + $qb = $this->connection->getQueryBuilder(); + $qb->insert('share') + ->values($shareValues) + ->execute(); + + return $this->getLastShareId(); + } + + /** + * @return int + */ + protected function getLastShareId() { + // select because lastInsertId does not work with OCI + $query = $this->connection->getQueryBuilder(); + $result = $query->select('id') + ->from('share') + ->orderBy('id', 'DESC') + ->execute(); + $row = $result->fetch(); + $result->closeCursor(); + return $row['id']; + } +} diff --git a/tests/lib/repair/repairinvalidsharestest.php b/tests/lib/repair/repairinvalidsharestest.php index 89a5ba470e1..005a2db2344 100644 --- a/tests/lib/repair/repairinvalidsharestest.php +++ b/tests/lib/repair/repairinvalidsharestest.php @@ -77,13 +77,7 @@ class RepairInvalidSharesTest extends TestCase { ]) ->execute(); - // select because lastInsertId does not work with OCI - $results = $this->connection->getQueryBuilder() - ->select('id') - ->from('share') - ->execute() - ->fetchAll(); - $bogusShareId = $results[0]['id']; + $bogusShareId = $this->getLastShareId(); // link share with expiration date $qb = $this->connection->getQueryBuilder(); @@ -119,5 +113,83 @@ class RepairInvalidSharesTest extends TestCase { $this->assertNull($userShare['expiration'], 'bogus expiration date was removed'); $this->assertNotNull($linkShare['expiration'], 'valid link share expiration date still there'); } + + /** + * Test remove shares where the parent share does not exist anymore + */ + public function testSharesNonExistingParent() { + $qb = $this->connection->getQueryBuilder(); + $shareValues = [ + 'share_type' => $qb->expr()->literal(Constants::SHARE_TYPE_USER), + 'share_with' => $qb->expr()->literal('recipientuser1'), + 'uid_owner' => $qb->expr()->literal('user1'), + 'item_type' => $qb->expr()->literal('folder'), + 'item_source' => $qb->expr()->literal(123), + 'item_target' => $qb->expr()->literal('/123'), + 'file_source' => $qb->expr()->literal(123), + 'file_target' => $qb->expr()->literal('/test'), + 'permissions' => $qb->expr()->literal(1), + 'stime' => $qb->expr()->literal(time()), + 'expiration' => $qb->expr()->literal('2015-09-25 00:00:00') + ]; + + // valid share + $qb = $this->connection->getQueryBuilder(); + $qb->insert('share') + ->values($shareValues) + ->execute(); + $parent = $this->getLastShareId(); + + // share with existing parent + $qb = $this->connection->getQueryBuilder(); + $qb->insert('share') + ->values(array_merge($shareValues, [ + 'parent' => $qb->expr()->literal($parent), + ]))->execute(); + $validChild = $this->getLastShareId(); + + // share with non-existing parent + $qb = $this->connection->getQueryBuilder(); + $qb->insert('share') + ->values(array_merge($shareValues, [ + 'parent' => $qb->expr()->literal($parent + 100), + ]))->execute(); + $invalidChild = $this->getLastShareId(); + + $query = $this->connection->getQueryBuilder(); + $result = $query->select('id') + ->from('share') + ->orderBy('id', 'ASC') + ->execute(); + $rows = $result->fetchAll(); + $this->assertSame([['id' => $parent], ['id' => $validChild], ['id' => $invalidChild]], $rows); + $result->closeCursor(); + + $this->repair->run(); + + $query = $this->connection->getQueryBuilder(); + $result = $query->select('id') + ->from('share') + ->orderBy('id', 'ASC') + ->execute(); + $rows = $result->fetchAll(); + $this->assertSame([['id' => $parent], ['id' => $validChild]], $rows); + $result->closeCursor(); + } + + /** + * @return int + */ + protected function getLastShareId() { + // select because lastInsertId does not work with OCI + $query = $this->connection->getQueryBuilder(); + $result = $query->select('id') + ->from('share') + ->orderBy('id', 'DESC') + ->execute(); + $row = $result->fetch(); + $result->closeCursor(); + return $row['id']; + } } diff --git a/tests/travis/changed_app.sh b/tests/travis/changed_app.sh new file mode 100755 index 00000000000..a4be0476573 --- /dev/null +++ b/tests/travis/changed_app.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# +# ownCloud +# +# @author Joas Schilling +# @author Thomas Müller +# @copyright 2015 Thomas Müller thomas.mueller@tmit.eu +# + +APP=$1 + +FOUND=$(git diff ${TRAVIS_COMMIT_RANGE} | grep -- "^+++ b/apps/$APP/") + +if [ "x$FOUND" != 'x' ]; then + echo "1" +else + echo "0" +fi diff --git a/tests/travis/test_for_app.sh b/tests/travis/test_for_app.sh deleted file mode 100755 index a97c66dba83..00000000000 --- a/tests/travis/test_for_app.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -# -# ownCloud -# -# @author Thomas Müller -# @copyright 2015 Thomas Müller thomas.mueller@tmit.eu -# - -set -e -APP=$1 - -if git diff ${TRAVIS_COMMIT_RANGE} | grep -- "^+++ b/apps/$APP/"; then - echo "Executing this test config ...." -else - echo "Test config is not relevant for this change. terminating" - exit 1 -fi |