diff options
27 files changed, 539 insertions, 200 deletions
diff --git a/apps/dav/lib/carddav/carddavbackend.php b/apps/dav/lib/carddav/carddavbackend.php index 56fa652d798..61bdec52479 100644 --- a/apps/dav/lib/carddav/carddavbackend.php +++ b/apps/dav/lib/carddav/carddavbackend.php @@ -776,7 +776,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { $query2->orWhere( $query2->expr()->andX( $query2->expr()->eq('cp.name', $query->createNamedParameter($property)), - $query2->expr()->like('cp.value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')) + $query2->expr()->ilike('cp.value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')) ) ); } diff --git a/apps/dav/tests/unit/carddav/carddavbackendtest.php b/apps/dav/tests/unit/carddav/carddavbackendtest.php index f920eb47b6e..401041d6e39 100644 --- a/apps/dav/tests/unit/carddav/carddavbackendtest.php +++ b/apps/dav/tests/unit/carddav/carddavbackendtest.php @@ -547,8 +547,8 @@ class CardDavBackendTest extends TestCase { ['John', ['FN'], ['John Doe', 'John M. Doe']], ['M. Doe', ['FN'], ['John M. Doe']], ['Do', ['FN'], ['John Doe', 'John M. Doe']], - // check if duplicates are handled correctly - ['John', ['FN', 'CLOUD'], ['John Doe', 'John M. Doe']], + 'check if duplicates are handled correctly' => ['John', ['FN', 'CLOUD'], ['John Doe', 'John M. Doe']], + 'case insensitive' => ['john', ['FN'], ['John Doe', 'John M. Doe']] ]; } diff --git a/apps/dav/tests/unit/connector/sabre/filesreportplugin.php b/apps/dav/tests/unit/connector/sabre/filesreportplugin.php index 83af45d3bcd..78f9e77063c 100644 --- a/apps/dav/tests/unit/connector/sabre/filesreportplugin.php +++ b/apps/dav/tests/unit/connector/sabre/filesreportplugin.php @@ -19,7 +19,7 @@ * */ -namespace OCA\DAV\Tests\Unit\Sabre\Connector; +namespace OCA\DAV\Tests\Unit\Connector\Sabre; use OCA\DAV\Connector\Sabre\FilesReportPlugin as FilesReportPluginImplementation; use Sabre\DAV\Exception\NotFound; @@ -369,7 +369,7 @@ class FilesReportPlugin extends \Test\TestCase { ['123', 'files'] ) ->willReturnMap([ - ['123', 'files', ['111', '222']], + ['123', 'files', 0, '', ['111', '222']], ]); $rules = [ @@ -391,8 +391,8 @@ class FilesReportPlugin extends \Test\TestCase { ['456', 'files'] ) ->willReturnMap([ - ['123', 'files', ['111', '222']], - ['456', 'files', ['222', '333']], + ['123', 'files', 0, '', ['111', '222']], + ['456', 'files', 0, '', ['222', '333']], ]); $rules = [ @@ -415,8 +415,8 @@ class FilesReportPlugin extends \Test\TestCase { ['456', 'files'] ) ->willReturnMap([ - ['123', 'files', ['111', '222']], - ['456', 'files', []], + ['123', 'files', 0, '', ['111', '222']], + ['456', 'files', 0, '', []], ]); $rules = [ @@ -439,8 +439,8 @@ class FilesReportPlugin extends \Test\TestCase { ['456', 'files'] ) ->willReturnMap([ - ['123', 'files', []], - ['456', 'files', ['111', '222']], + ['123', 'files', 0, '', []], + ['456', 'files', 0, '', ['111', '222']], ]); $rules = [ @@ -464,9 +464,9 @@ class FilesReportPlugin extends \Test\TestCase { ['789', 'files'] ) ->willReturnMap([ - ['123', 'files', ['111', '222']], - ['456', 'files', ['333']], - ['789', 'files', ['111', '222']], + ['123', 'files', 0, '', ['111', '222']], + ['456', 'files', 0, '', ['333']], + ['789', 'files', 0, '', ['111', '222']], ]); $rules = [ diff --git a/apps/files/js/file-upload.js b/apps/files/js/file-upload.js index 8ba294e2a7f..bd80afd072c 100644 --- a/apps/files/js/file-upload.js +++ b/apps/files/js/file-upload.js @@ -472,7 +472,11 @@ OC.Upload = { OC.Upload.showUploadCancelMessage(); } else { // HTTP connection problem - OC.Notification.showTemporary(data.errorThrown, {timeout: 10}); + var message = t('files', 'Error uploading file "{fileName}": {message}', { + fileName: data.files[0].name, + message: data.errorThrown + }); + OC.Notification.show(message, {timeout: 0, type: 'error'}); if (data.result) { var result = JSON.parse(data.result); if (result && result[0] && result[0].data && result[0].data.code === 'targetnotfound') { diff --git a/apps/files/tests/js/fileactionsSpec.js b/apps/files/tests/js/fileactionsSpec.js index a905a4d969d..4f4d4d3d197 100644 --- a/apps/files/tests/js/fileactionsSpec.js +++ b/apps/files/tests/js/fileactionsSpec.js @@ -20,9 +20,10 @@ */ describe('OCA.Files.FileActions tests', function() { - var fileList, fileActions; + var fileList, fileActions, clock; beforeEach(function() { + clock = sinon.useFakeTimers(); // init horrible parameters var $body = $('#testArea'); $body.append('<input type="hidden" id="dir" value="/subdir"></input>'); @@ -63,6 +64,7 @@ describe('OCA.Files.FileActions tests', function() { fileActions = null; fileList.destroy(); fileList = undefined; + clock.restore(); $('#dir, #permissions, #filestable').remove(); }); it('calling clear() clears file actions', function() { diff --git a/apps/systemtags/tests/js/systemtagsinfoviewSpec.js b/apps/systemtags/tests/js/systemtagsinfoviewSpec.js index 0fb4e7b22c2..27724822c2e 100644 --- a/apps/systemtags/tests/js/systemtagsinfoviewSpec.js +++ b/apps/systemtags/tests/js/systemtagsinfoviewSpec.js @@ -22,14 +22,17 @@ describe('OCA.SystemTags.SystemTagsInfoView tests', function() { var isAdminStub; var view; + var clock; beforeEach(function() { + clock = sinon.useFakeTimers(); view = new OCA.SystemTags.SystemTagsInfoView(); $('#testArea').append(view.$el); isAdminStub = sinon.stub(OC, 'isUserAdmin').returns(true); }); afterEach(function() { isAdminStub.restore(); + clock.restore(); view.remove(); view = undefined; }); diff --git a/autotest-external.sh b/autotest-external.sh index 0a3fafbd2a6..643153bb064 100755 --- a/autotest-external.sh +++ b/autotest-external.sh @@ -148,7 +148,7 @@ EOF # trigger installation echo "Installing ...." - ./occ maintenance:install --database=$1 --database-name=$DATABASENAME --database-host=localhost --database-user=$DATABASEUSER --database-pass=owncloud --database-table-prefix=oc_ --admin-user=$ADMINLOGIN --admin-pass=admin --data-dir=$DATADIR + ./occ maintenance:install -vvv --database=$1 --database-name=$DATABASENAME --database-host=localhost --database-user=$DATABASEUSER --database-pass=owncloud --database-table-prefix=oc_ --admin-user=$ADMINLOGIN --admin-pass=admin --data-dir=$DATADIR #test execution echo "Testing with $1 ..." @@ -161,7 +161,7 @@ EOF rm -rf "coverage-external-html-$1" mkdir "coverage-external-html-$1" # just enable files_external - php ../occ app:enable files_external + php ../occ app:enable -vvv files_external if [[ "$_XDEBUG_CONFIG" ]]; then export XDEBUG_CONFIG=$_XDEBUG_CONFIG fi diff --git a/autotest.sh b/autotest.sh index d8d75817712..ba6ec383866 100755 --- a/autotest.sh +++ b/autotest.sh @@ -251,7 +251,7 @@ function execute_tests { # trigger installation echo "Installing ...." - "$PHP" ./occ maintenance:install --database="$_DB" --database-name="$DATABASENAME" --database-host="$DATABASEHOST" --database-user="$DATABASEUSER" --database-pass=owncloud --database-table-prefix=oc_ --admin-user="$ADMINLOGIN" --admin-pass=admin --data-dir="$DATADIR" + "$PHP" ./occ maintenance:install -vvv --database="$_DB" --database-name="$DATABASENAME" --database-host="$DATABASEHOST" --database-user="$DATABASEUSER" --database-pass=owncloud --database-table-prefix=oc_ --admin-user="$ADMINLOGIN" --admin-pass=admin --data-dir="$DATADIR" #test execution echo "Testing with $DB ..." diff --git a/build/integration/features/sharing-v1.feature b/build/integration/features/sharing-v1.feature index 1a1a5c1981a..e16de8b6b11 100644 --- a/build/integration/features/sharing-v1.feature +++ b/build/integration/features/sharing-v1.feature @@ -495,3 +495,14 @@ Feature: sharing And user "user2" does not exist And user "user1" should see following elements | /myFOLDER/myTMP/ | + + Scenario: Check quota of owners parent directory of a shared file + Given using dav path "remote.php/webdav" + And As an "admin" + And user "user0" exists + And user "user1" exists + And user "user1" has a quota of "0" + And User "user0" moved file "/welcome.txt" to "/myfile.txt" + And file "myfile.txt" of user "user0" is shared with user "user1" + When User "user1" uploads file "data/textfile.txt" to "/myfile.txt" + Then the HTTP status code should be "204" diff --git a/core/css/styles.css b/core/css/styles.css index e78db6fde01..4edb29a8c6e 100644 --- a/core/css/styles.css +++ b/core/css/styles.css @@ -650,7 +650,7 @@ td.avatar { width: 100%; text-align: center; } -#notification, #update-notification { +#notification { margin: 0 auto; max-width: 60%; z-index: 8000; @@ -665,10 +665,28 @@ td.avatar { -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=90)"; opacity: .9; } -#notification span, #update-notification span { +#notification span { cursor: pointer; margin-left: 1em; } +#notification { + overflow-x: hidden; + overflow-y: auto; + max-height: 100px; +} +#notification .row { + position: relative; +} +#notification .row .close { + display: inline-block; + vertical-align: middle; + position: absolute; + right: 0; + top: 0; +} +#notification .row.closeable { + padding-right: 20px; +} tr .action:not(.permanent), .selectedActions a { diff --git a/core/js/js.js b/core/js/js.js index fac9c45f668..e90ceaf4e18 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -988,7 +988,11 @@ OC.msg = { OC.Notification={ queuedNotifications: [], getDefaultNotificationFunction: null, - notificationTimer: 0, + + /** + * @type Array.<int> array of notification timers + */ + notificationTimers: [], /** * @param callback @@ -999,25 +1003,64 @@ OC.Notification={ }, /** - * Hides a notification - * @param callback - * @todo Write documentation + * Hides a notification. + * + * If a row is given, only hide that one. + * If no row is given, hide all notifications. + * + * @param {jQuery} [$row] notification row + * @param {Function} [callback] callback */ - hide: function(callback) { - $('#notification').fadeOut('400', function(){ - if (OC.Notification.isHidden()) { - if (OC.Notification.getDefaultNotificationFunction) { - OC.Notification.getDefaultNotificationFunction.call(); - } - } + hide: function($row, callback) { + var self = this; + var $notification = $('#notification'); + + if (_.isFunction($row)) { + // first arg is the callback + callback = $row; + $row = undefined; + } + + if (!$row) { + console.warn('Missing argument $row in OC.Notification.hide() call, caller needs to be adjusted to only dismiss its own notification'); + // assume that the row to be hidden is the first one + $row = $notification.find('.row:first'); + } + + if ($row && $notification.find('.row').length > 1) { + // remove the row directly + $row.remove(); if (callback) { callback.call(); } - $('#notification').empty(); - if(OC.Notification.queuedNotifications.length > 0){ - OC.Notification.showHtml(OC.Notification.queuedNotifications[0]); - OC.Notification.queuedNotifications.shift(); + return; + } + + _.defer(function() { + // fade out is supposed to only fade when there is a single row + // however, some code might call hide() and show() directly after, + // which results in more than one element + // in this case, simply delete that one element that was supposed to + // fade out + // + // FIXME: remove once all callers are adjusted to only hide their own notifications + if ($notification.find('.row').length > 1) { + $row.remove(); + return; } + + // else, fade out whatever was present + $notification.fadeOut('400', function(){ + if (self.isHidden()) { + if (self.getDefaultNotificationFunction) { + self.getDefaultNotificationFunction.call(); + } + } + if (callback) { + callback.call(); + } + $notification.empty(); + }); }); }, @@ -1025,66 +1068,93 @@ OC.Notification={ * Shows a notification as HTML without being sanitized before. * If you pass unsanitized user input this may lead to a XSS vulnerability. * Consider using show() instead of showHTML() + * * @param {string} html Message to display - */ - showHtml: function(html) { - var notification = $('#notification'); - if((notification.filter('span.undo').length == 1) || OC.Notification.isHidden()){ - notification.html(html); - notification.fadeIn().css('display','inline-block'); - }else{ - OC.Notification.queuedNotifications.push(html); + * @param {Object} [options] options + * @param {string] [options.type] notification type + * @param {int} [options.timeout=0] timeout value, defaults to 0 (permanent) + * @return {jQuery} jQuery element for notification row + */ + showHtml: function(html, options) { + options = options || {}; + _.defaults(options, { + timeout: 0 + }); + + var self = this; + var $notification = $('#notification'); + if (this.isHidden()) { + $notification.fadeIn().css('display','inline-block'); + } + var $row = $('<div class="row"></div>'); + if (options.type) { + $row.addClass('type-' + options.type); + } + if (options.type === 'error') { + // add a close button + var $closeButton = $('<a class="action close icon-close" href="#"></a>'); + $closeButton.attr('alt', t('core', 'Dismiss')); + $row.append($closeButton); + $closeButton.one('click', function() { + self.hide($row); + return false; + }); + $row.addClass('closeable'); } + + $row.prepend(html); + $notification.append($row); + + if(options.timeout > 0) { + // register timeout to vanish notification + this.notificationTimers.push(setTimeout(function() { + self.hide($row); + }, (options.timeout * 1000))); + } + + return $row; }, /** * Shows a sanitized notification + * * @param {string} text Message to display + * @param {Object} [options] options + * @param {string] [options.type] notification type + * @param {int} [options.timeout=0] timeout value, defaults to 0 (permanent) + * @return {jQuery} jQuery element for notification row */ - show: function(text) { - var notification = $('#notification'); - if((notification.filter('span.undo').length == 1) || OC.Notification.isHidden()){ - notification.text(text); - notification.fadeIn().css('display','inline-block'); - }else{ - OC.Notification.queuedNotifications.push($('<div/>').text(text).html()); - } + show: function(text, options) { + return this.showHtml($('<div/>').text(text).html(), options); }, - /** * Shows a notification that disappears after x seconds, default is * 7 seconds + * * @param {string} text Message to show * @param {array} [options] options array * @param {int} [options.timeout=7] timeout in seconds, if this is 0 it will show the message permanently * @param {boolean} [options.isHTML=false] an indicator for HTML notifications (true) or text (false) + * @param {string] [options.type] notification type */ showTemporary: function(text, options) { + var self = this; var defaults = { - isHTML: false, - timeout: 7 - }, - options = options || {}; + isHTML: false, + timeout: 7 + }; + options = options || {}; // merge defaults with passed in options _.defaults(options, defaults); - // clear previous notifications - OC.Notification.hide(); - if(OC.Notification.notificationTimer) { - clearTimeout(OC.Notification.notificationTimer); - } - + var $row; if(options.isHTML) { - OC.Notification.showHtml(text); + $row = this.showHtml(text, options); } else { - OC.Notification.show(text); - } - - if(options.timeout > 0) { - // register timeout to vanish notification - OC.Notification.notificationTimer = setTimeout(OC.Notification.hide, (options.timeout * 1000)); + $row = this.show(text, options); } + return $row; }, /** @@ -1092,7 +1162,7 @@ OC.Notification={ * @return {boolean} */ isHidden: function() { - return ($("#notification").text() === ''); + return !$("#notification").find('.row').length; } }; diff --git a/core/js/tests/specs/coreSpec.js b/core/js/tests/specs/coreSpec.js index 32eb8df32d1..774c2fdc72f 100644 --- a/core/js/tests/specs/coreSpec.js +++ b/core/js/tests/specs/coreSpec.js @@ -747,100 +747,181 @@ describe('Core base tests', function() { }); }); describe('Notifications', function() { - beforeEach(function(){ - notificationMock = sinon.mock(OC.Notification); + var showSpy; + var showHtmlSpy; + var hideSpy; + var clock; + + beforeEach(function() { + clock = sinon.useFakeTimers(); + showSpy = sinon.spy(OC.Notification, 'show'); + showHtmlSpy = sinon.spy(OC.Notification, 'showHtml'); + hideSpy = sinon.spy(OC.Notification, 'hide'); + + $('#testArea').append('<div id="notification"></div>'); }); - afterEach(function(){ - // verify that all expectations are met - notificationMock.verify(); - // restore mocked methods - notificationMock.restore(); - // clean up the global variable - delete notificationMock; + afterEach(function() { + showSpy.restore(); + showHtmlSpy.restore(); + hideSpy.restore(); + // jump past animations + clock.tick(10000); + clock.restore(); }); - it('Should show a plain text notification' , function() { - // one is shown ... - notificationMock.expects('show').once().withExactArgs('My notification test'); - // ... but not the HTML one - notificationMock.expects('showHtml').never(); + describe('showTemporary', function() { + it('shows a plain text notification with default timeout', function() { + var $row = OC.Notification.showTemporary('My notification test'); - OC.Notification.showTemporary('My notification test'); + expect(showSpy.calledOnce).toEqual(true); + expect(showSpy.firstCall.args[0]).toEqual('My notification test'); + expect(showSpy.firstCall.args[1]).toEqual({isHTML: false, timeout: 7}); - // verification is done in afterEach + expect($row).toBeDefined(); + expect($row.text()).toEqual('My notification test'); + }); + it('shows a HTML notification with default timeout', function() { + var $row = OC.Notification.showTemporary('<a>My notification test</a>', { isHTML: true }); + + expect(showSpy.notCalled).toEqual(true); + expect(showHtmlSpy.calledOnce).toEqual(true); + expect(showHtmlSpy.firstCall.args[0]).toEqual('<a>My notification test</a>'); + expect(showHtmlSpy.firstCall.args[1]).toEqual({isHTML: true, timeout: 7}); + + expect($row).toBeDefined(); + expect($row.text()).toEqual('My notification test'); + }); + it('hides itself after 7 seconds', function() { + var $row = OC.Notification.showTemporary(''); + + // travel in time +7000 milliseconds + clock.tick(7000); + + expect(hideSpy.calledOnce).toEqual(true); + expect(hideSpy.firstCall.args[0]).toEqual($row); + }); }); - it('Should show a HTML notification' , function() { - // no plain is shown ... - notificationMock.expects('show').never(); - // ... but one HTML notification - notificationMock.expects('showHtml').once().withExactArgs('<a>My notification test</a>'); + describe('show', function() { + it('hides itself after a given time', function() { + OC.Notification.show('', { timeout: 10 }); + + // travel in time +9 seconds + clock.tick(9000); - OC.Notification.showTemporary('<a>My notification test</a>', { isHTML: true }); + expect(hideSpy.notCalled).toEqual(true); - // verification is done in afterEach + // travel in time +1 seconds + clock.tick(1000); + + expect(hideSpy.calledOnce).toEqual(true); + }); + it('does not hide itself after a given time if a timeout of 0 is defined', function() { + OC.Notification.show('', { timeout: 0 }); + + // travel in time +1000 seconds + clock.tick(1000000); + + expect(hideSpy.notCalled).toEqual(true); + }); + it('does not hide itself if no timeout given to show', function() { + OC.Notification.show(''); + + // travel in time +1000 seconds + clock.tick(1000000); + + expect(hideSpy.notCalled).toEqual(true); + }); }); - it('Should hide previous notification and hide itself after 7 seconds' , function() { - var clock = sinon.useFakeTimers(); + it('cumulates several notifications', function() { + var $row1 = OC.Notification.showTemporary('One'); + var $row2 = OC.Notification.showTemporary('Two', {timeout: 2}); + var $row3 = OC.Notification.showTemporary('Three'); + + var $el = $('#notification'); + var $rows = $el.find('.row'); + expect($rows.length).toEqual(3); - // previous notifications get hidden - notificationMock.expects('hide').once(); + expect($rows.eq(0).is($row1)).toEqual(true); + expect($rows.eq(1).is($row2)).toEqual(true); + expect($rows.eq(2).is($row3)).toEqual(true); - OC.Notification.showTemporary(''); + clock.tick(3000); - // verify the first call - notificationMock.verify(); + $rows = $el.find('.row'); + expect($rows.length).toEqual(2); - // expect it a second time - notificationMock.expects('hide').once(); + expect($rows.eq(0).is($row1)).toEqual(true); + expect($rows.eq(1).is($row3)).toEqual(true); + }); + it('shows close button for error types', function() { + var $row = OC.Notification.showTemporary('One'); + var $rowError = OC.Notification.showTemporary('Two', {type: 'error'}); + expect($row.find('.close').length).toEqual(0); + expect($rowError.find('.close').length).toEqual(1); - // travel in time +7000 milliseconds - clock.tick(7000); + // after clicking, row is gone + $rowError.find('.close').click(); - // verification is done in afterEach + var $rows = $('#notification').find('.row'); + expect($rows.length).toEqual(1); + expect($rows.eq(0).is($row)).toEqual(true); }); - it('Should hide itself after a given time' , function() { - var clock = sinon.useFakeTimers(); + it('fades out the last notification but not the other ones', function() { + var fadeOutStub = sinon.stub($.fn, 'fadeOut'); + var $row1 = OC.Notification.show('One', {type: 'error'}); + var $row2 = OC.Notification.show('Two', {type: 'error'}); + OC.Notification.showTemporary('Three', {timeout: 2}); - // previous notifications get hidden - notificationMock.expects('hide').once(); + var $el = $('#notification'); + var $rows = $el.find('.row'); + expect($rows.length).toEqual(3); - OC.Notification.showTemporary('', { timeout: 10 }); + clock.tick(3000); - // verify the first call - notificationMock.verify(); + $rows = $el.find('.row'); + expect($rows.length).toEqual(2); - // expect to not be called after 9 seconds - notificationMock.expects('hide').never(); + $row1.find('.close').click(); + clock.tick(1000); - // travel in time +9 seconds - clock.tick(9000); - // verify this - notificationMock.verify(); + expect(fadeOutStub.notCalled).toEqual(true); - // expect the second call one second later - notificationMock.expects('hide').once(); - // travel in time +1 seconds + $row2.find('.close').click(); clock.tick(1000); + expect(fadeOutStub.calledOnce).toEqual(true); - // verification is done in afterEach + expect($el.is(':empty')).toEqual(false); + fadeOutStub.yield(); + expect($el.is(':empty')).toEqual(true); + + fadeOutStub.restore(); }); - it('Should not hide itself after a given time if a timeout of 0 is defined' , function() { - var clock = sinon.useFakeTimers(); + it('hides the first notification when calling hide without arguments', function() { + var $row1 = OC.Notification.show('One'); + var $row2 = OC.Notification.show('Two'); - // previous notifications get hidden - notificationMock.expects('hide').once(); + var $el = $('#notification'); + var $rows = $el.find('.row'); + expect($rows.length).toEqual(2); - OC.Notification.showTemporary('', { timeout: 0 }); + OC.Notification.hide(); - // verify the first call - notificationMock.verify(); + $rows = $el.find('.row'); + expect($rows.length).toEqual(1); + expect($rows.eq(0).is($row2)).toEqual(true); + }); + it('hides the given notification when calling hide with argument', function() { + var $row1 = OC.Notification.show('One'); + var $row2 = OC.Notification.show('Two'); - // expect to not be called after 1000 seconds - notificationMock.expects('hide').never(); + var $el = $('#notification'); + var $rows = $el.find('.row'); + expect($rows.length).toEqual(2); - // travel in time +1000 seconds - clock.tick(1000000); + OC.Notification.hide($row2); - // verification is done in afterEach + $rows = $el.find('.row'); + expect($rows.length).toEqual(1); + expect($rows.eq(0).is($row1)).toEqual(true); }); }); describe('global ajax errors', function() { diff --git a/core/js/tests/specs/systemtags/systemtagsinputfieldSpec.js b/core/js/tests/specs/systemtags/systemtagsinputfieldSpec.js index d62ef672f4d..aadf0de53f2 100644 --- a/core/js/tests/specs/systemtags/systemtagsinputfieldSpec.js +++ b/core/js/tests/specs/systemtags/systemtagsinputfieldSpec.js @@ -20,9 +20,10 @@ */ describe('OC.SystemTags.SystemTagsInputField tests', function() { - var view, select2Stub; + var view, select2Stub, clock; beforeEach(function() { + clock = sinon.useFakeTimers(); var $container = $('<div class="testInputContainer"></div>'); select2Stub = sinon.stub($.fn, 'select2'); select2Stub.returnsThis(); @@ -31,6 +32,7 @@ describe('OC.SystemTags.SystemTagsInputField tests', function() { afterEach(function() { select2Stub.restore(); OC.SystemTags.collection.reset(); + clock.restore(); view.remove(); view = undefined; }); diff --git a/db_structure.xml b/db_structure.xml index dbbfa8c7a4d..b1242171127 100644 --- a/db_structure.xml +++ b/db_structure.xml @@ -1249,11 +1249,10 @@ <!-- object id (ex: file id for files)--> <field> <name>objectid</name> - <type>integer</type> - <default>0</default> + <type>text</type> + <default></default> <notnull>true</notnull> - <unsigned>true</unsigned> - <length>4</length> + <length>64</length> </field> <!-- object type (ex: "files")--> diff --git a/lib/private/db/querybuilder/expressionbuilder/expressionbuilder.php b/lib/private/db/querybuilder/expressionbuilder/expressionbuilder.php index 7ab4c03d97c..6fb58ac3c9d 100644 --- a/lib/private/db/querybuilder/expressionbuilder/expressionbuilder.php +++ b/lib/private/db/querybuilder/expressionbuilder/expressionbuilder.php @@ -277,6 +277,23 @@ class ExpressionBuilder implements IExpressionBuilder { } /** + * Creates a ILIKE() comparison expression with the given arguments. + * + * @param string $x Field in string format to be inspected by ILIKE() comparison. + * @param mixed $y Argument to be used in ILIKE() comparison. + * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants + * required when comparing text fields for oci compatibility + * + * @return string + * @since 9.0.0 + */ + public function iLike($x, $y, $type = null) { + $x = $this->helper->quoteColumnName($x); + $y = $this->helper->quoteColumnName($y); + return $this->expressionBuilder->comparison("LOWER($x)", 'LIKE', "LOWER($y)"); + } + + /** * Creates a NOT LIKE() comparison expression with the given arguments. * * @param string $x Field in string format to be inspected by NOT LIKE() comparison. diff --git a/lib/private/db/querybuilder/expressionbuilder/mysqlexpressionbuilder.php b/lib/private/db/querybuilder/expressionbuilder/mysqlexpressionbuilder.php new file mode 100644 index 00000000000..8164d9623b8 --- /dev/null +++ b/lib/private/db/querybuilder/expressionbuilder/mysqlexpressionbuilder.php @@ -0,0 +1,39 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * + * @copyright Copyright (c) 2016, 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\DB\QueryBuilder\ExpressionBuilder; + + +use OC\DB\QueryBuilder\QueryFunction; +use OCP\DB\QueryBuilder\IQueryBuilder; + +class MySqlExpressionBuilder extends ExpressionBuilder { + + /** + * @inheritdoc + */ + public function iLike($x, $y, $type = null) { + $x = $this->helper->quoteColumnName($x); + $y = $this->helper->quoteColumnName($y); + return $this->expressionBuilder->comparison($x, ' COLLATE utf8_general_ci LIKE', $y); + } + +} diff --git a/lib/private/db/querybuilder/expressionbuilder/ociexpressionbuilder.php b/lib/private/db/querybuilder/expressionbuilder/ociexpressionbuilder.php index 6a6d0f455f6..bd7daa23911 100644 --- a/lib/private/db/querybuilder/expressionbuilder/ociexpressionbuilder.php +++ b/lib/private/db/querybuilder/expressionbuilder/ociexpressionbuilder.php @@ -149,4 +149,13 @@ class OCIExpressionBuilder extends ExpressionBuilder { return parent::castColumn($column, $type); } + + /** + * @inheritdoc + */ + public function iLike($x, $y, $type = null) { + $x = $this->helper->quoteColumnName($x); + $y = $this->helper->quoteColumnName($y); + return new QueryFunction('REGEXP_LIKE('.$x.', \'^\' || REPLACE('.$y.', \'%\', \'.*\') || \'$\', \'i\')'); + } } diff --git a/lib/private/db/querybuilder/expressionbuilder/pgsqlexpressionbuilder.php b/lib/private/db/querybuilder/expressionbuilder/pgsqlexpressionbuilder.php index 8a0b68db998..ac2d7bf2421 100644 --- a/lib/private/db/querybuilder/expressionbuilder/pgsqlexpressionbuilder.php +++ b/lib/private/db/querybuilder/expressionbuilder/pgsqlexpressionbuilder.php @@ -42,4 +42,14 @@ class PgSqlExpressionBuilder extends ExpressionBuilder { return parent::castColumn($column, $type); } + + /** + * @inheritdoc + */ + public function iLike($x, $y, $type = null) { + $x = $this->helper->quoteColumnName($x); + $y = $this->helper->quoteColumnName($y); + return $this->expressionBuilder->comparison($x, 'ILIKE', $y); + } + } diff --git a/lib/private/db/querybuilder/querybuilder.php b/lib/private/db/querybuilder/querybuilder.php index ff31ffbc043..de803116dc4 100644 --- a/lib/private/db/querybuilder/querybuilder.php +++ b/lib/private/db/querybuilder/querybuilder.php @@ -21,9 +21,11 @@ namespace OC\DB\QueryBuilder; +use Doctrine\DBAL\Platforms\MySqlPlatform; use Doctrine\DBAL\Platforms\PostgreSqlPlatform; use OC\DB\OracleConnection; use OC\DB\QueryBuilder\ExpressionBuilder\ExpressionBuilder; +use OC\DB\QueryBuilder\ExpressionBuilder\MySqlExpressionBuilder; use OC\DB\QueryBuilder\ExpressionBuilder\OCIExpressionBuilder; use OC\DB\QueryBuilder\ExpressionBuilder\PgSqlExpressionBuilder; use OCP\DB\QueryBuilder\IQueryBuilder; @@ -91,6 +93,8 @@ class QueryBuilder implements IQueryBuilder { return new OCIExpressionBuilder($this->connection); } else if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) { return new PgSqlExpressionBuilder($this->connection); + } else if ($this->connection->getDatabasePlatform() instanceof MySqlPlatform) { + return new MySqlExpressionBuilder($this->connection); } else { return new ExpressionBuilder($this->connection); } diff --git a/lib/private/files/utils/scanner.php b/lib/private/files/utils/scanner.php index 59673a306cb..bc815f5f6da 100644 --- a/lib/private/files/utils/scanner.php +++ b/lib/private/files/utils/scanner.php @@ -119,6 +119,10 @@ class Scanner extends PublicEmitter { if (is_null($mount->getStorage())) { continue; } + // don't scan the root storage + if ($mount->getStorage()->instanceOfStorage('\OC\Files\Storage\Local') && $mount->getMountPoint() === '/') { + continue; + } $scanner = $mount->getStorage()->getScanner(); $this->attachListener($mount); $scanner->backgroundScan(); diff --git a/lib/private/systemtag/systemtagobjectmapper.php b/lib/private/systemtag/systemtagobjectmapper.php index 1efb4f0f6e0..586351cf8c1 100644 --- a/lib/private/systemtag/systemtagobjectmapper.php +++ b/lib/private/systemtag/systemtagobjectmapper.php @@ -95,7 +95,7 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper { /** * {@inheritdoc} */ - public function getObjectIdsForTags($tagIds, $objectType) { + public function getObjectIdsForTags($tagIds, $objectType, $limit = 0, $offset = '') { if (!is_array($tagIds)) { $tagIds = [$tagIds]; } @@ -103,12 +103,23 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper { $this->assertTagsExist($tagIds); $query = $this->connection->getQueryBuilder(); - $query->select($query->createFunction('DISTINCT(`objectid`)')) + $query->selectDistinct('objectid') ->from(self::RELATION_TABLE) - ->where($query->expr()->in('systemtagid', $query->createParameter('tagids'))) - ->andWhere($query->expr()->eq('objecttype', $query->createParameter('objecttype'))) - ->setParameter('tagids', $tagIds, IQueryBuilder::PARAM_INT_ARRAY) - ->setParameter('objecttype', $objectType); + ->where($query->expr()->in('systemtagid', $query->createNamedParameter($tagIds, IQueryBuilder::PARAM_INT_ARRAY))) + ->andWhere($query->expr()->eq('objecttype', $query->createNamedParameter($objectType))); + + if ($limit) { + if (sizeof($tagIds) !== 1) { + throw new \InvalidArgumentException('Limit is only allowed with a single tag'); + } + + $query->setMaxResults($limit) + ->orderBy('objectid', 'ASC'); + + if ($offset !== '') { + $query->andWhere($query->expr()->gt('objectid', $query->createNamedParameter($offset))); + } + } $objectIds = []; @@ -208,7 +219,7 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper { ->where($query->expr()->in('objectid', $query->createParameter('objectids'))) ->andWhere($query->expr()->eq('objecttype', $query->createParameter('objecttype'))) ->andWhere($query->expr()->eq('systemtagid', $query->createParameter('tagid'))) - ->setParameter('objectids', $objIds, IQueryBuilder::PARAM_INT_ARRAY) + ->setParameter('objectids', $objIds, IQueryBuilder::PARAM_STR_ARRAY) ->setParameter('tagid', $tagId) ->setParameter('objecttype', $objectType); diff --git a/lib/public/db/querybuilder/iexpressionbuilder.php b/lib/public/db/querybuilder/iexpressionbuilder.php index 4b53a0e0b8b..0ed15bf4398 100644 --- a/lib/public/db/querybuilder/iexpressionbuilder.php +++ b/lib/public/db/querybuilder/iexpressionbuilder.php @@ -264,6 +264,19 @@ interface IExpressionBuilder { public function notLike($x, $y, $type = null); /** + * Creates a ILIKE() comparison expression with the given arguments. + * + * @param string $x Field in string format to be inspected by ILIKE() comparison. + * @param mixed $y Argument to be used in ILIKE() comparison. + * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants + * required when comparing text fields for oci compatibility + * + * @return string + * @since 9.0.0 + */ + public function iLike($x, $y, $type = null); + + /** * Creates a IN () comparison expression with the given arguments. * * @param string $x The field in string format to be inspected by IN() comparison. diff --git a/lib/public/systemtag/isystemtagobjectmapper.php b/lib/public/systemtag/isystemtagobjectmapper.php index 8db5cdd31aa..59b988a3656 100644 --- a/lib/public/systemtag/isystemtagobjectmapper.php +++ b/lib/public/systemtag/isystemtagobjectmapper.php @@ -57,15 +57,19 @@ interface ISystemTagObjectMapper { * * @param string|array $tagIds Tag id or array of tag ids. * @param string $objectType object type + * @param int $limit Count of object ids you want to get + * @param string $offset The last object id you already received * * @return string[] array of object ids or empty array if none found * * @throws \OCP\SystemTag\TagNotFoundException if at least one of the * given tags does not exist + * @throws \InvalidArgumentException When a limit is specified together with + * multiple tag ids * * @since 9.0.0 */ - public function getObjectIdsForTags($tagIds, $objectType); + public function getObjectIdsForTags($tagIds, $objectType, $limit = 0, $offset = ''); /** * Assign the given tags to the given object. diff --git a/settings/js/users/groups.js b/settings/js/users/groups.js index 2639191d918..27c41884504 100644 --- a/settings/js/users/groups.js +++ b/settings/js/users/groups.js @@ -142,8 +142,8 @@ GroupList = { .text(result.groupname)); } GroupList.toggleAddGroup(); - }).fail(function(result, textStatus, errorThrown) { - OC.dialogs.alert(result.responseJSON.message, t('settings', 'Error creating group')); + }).fail(function(result) { + OC.Notification.showTemporary(t('settings', 'Error creating group: {message}', {message: result.responseJSON.message})); }); }, @@ -245,9 +245,9 @@ GroupList = { isGroupNameValid: function (groupname) { if ($.trim(groupname) === '') { - OC.dialogs.alert( - t('settings', 'A valid group name must be provided'), - t('settings', 'Error creating group')); + OC.Notification.showTemporary(t('settings', 'Error creating group: {message}', { + message: t('settings', 'A valid group name must be provided') + })); return false; } return true; diff --git a/settings/js/users/users.js b/settings/js/users/users.js index 306e3952e53..261d9a8eb52 100644 --- a/settings/js/users/users.js +++ b/settings/js/users/users.js @@ -767,24 +767,24 @@ $(document).ready(function () { var password = $('#newuserpassword').val(); var email = $('#newemail').val(); if ($.trim(username) === '') { - OC.dialogs.alert( - t('settings', 'A valid username must be provided'), - t('settings', 'Error creating user')); + OC.Notification.showTemporary(t('settings', 'Error creating user: {message}', { + message: t('settings', 'A valid username must be provided') + })); return false; } if ($.trim(password) === '') { - OC.dialogs.alert( - t('settings', 'A valid password must be provided'), - t('settings', 'Error creating user')); + OC.Notification.showTemporary(t('settings', 'Error creating user: {message}', { + message: t('settings', 'A valid password must be provided') + })); return false; } if(!$('#CheckboxMailOnUserCreate').is(':checked')) { email = ''; } if ($('#CheckboxMailOnUserCreate').is(':checked') && $.trim(email) === '') { - OC.dialogs.alert( - t('settings', 'A valid email must be provided'), - t('settings', 'Error creating user')); + OC.Notification.showTemporary( t('settings', 'Error creating user: {message}', { + message: t('settings', 'A valid email must be provided') + })); return false; } @@ -822,8 +822,10 @@ $(document).ready(function () { } $('#newusername').focus(); GroupList.incEveryoneCount(); - }).fail(function(result, textStatus, errorThrown) { - OC.dialogs.alert(result.responseJSON.message, t('settings', 'Error creating user')); + }).fail(function(result) { + OC.Notification.showTemporary(t('settings', 'Error creating user: {message}', { + message: result.responseJSON.message + })); }).success(function(){ $('#newuser').get(0).reset(); }); diff --git a/tests/lib/systemtag/systemtagobjectmappertest.php b/tests/lib/systemtag/systemtagobjectmappertest.php index 5c8204f6a87..861eb240674 100644 --- a/tests/lib/systemtag/systemtagobjectmappertest.php +++ b/tests/lib/systemtag/systemtagobjectmappertest.php @@ -102,10 +102,10 @@ class SystemTagObjectMapperTest extends TestCase { return $result; })); - $this->tagMapper->assignTags(1, 'testtype', $this->tag1->getId()); - $this->tagMapper->assignTags(1, 'testtype', $this->tag2->getId()); - $this->tagMapper->assignTags(2, 'testtype', $this->tag1->getId()); - $this->tagMapper->assignTags(3, 'anothertype', $this->tag1->getId()); + $this->tagMapper->assignTags('1', 'testtype', $this->tag1->getId()); + $this->tagMapper->assignTags('1', 'testtype', $this->tag2->getId()); + $this->tagMapper->assignTags('2', 'testtype', $this->tag1->getId()); + $this->tagMapper->assignTags('3', 'anothertype', $this->tag1->getId()); } public function tearDown() { @@ -121,15 +121,15 @@ class SystemTagObjectMapperTest extends TestCase { public function testGetTagsForObjects() { $tagIdMapping = $this->tagMapper->getTagIdsForObjects( - [1, 2, 3, 4], + ['1', '2', '3', '4'], 'testtype' ); $this->assertEquals([ - 1 => [$this->tag1->getId(), $this->tag2->getId()], - 2 => [$this->tag1->getId()], - 3 => [], - 4 => [], + '1' => [$this->tag1->getId(), $this->tag2->getId()], + '2' => [$this->tag1->getId()], + '3' => [], + '4' => [], ], $tagIdMapping); } @@ -140,7 +140,43 @@ class SystemTagObjectMapperTest extends TestCase { ); $this->assertEquals([ + '1', + '2', + ], $objectIds); + } + + public function testGetObjectsForTagsLimit() { + $objectIds = $this->tagMapper->getObjectIdsForTags( + [$this->tag1->getId()], + 'testtype', + 1 + ); + + $this->assertEquals([ 1, + ], $objectIds); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testGetObjectsForTagsLimitWithMultipleTags() { + $this->tagMapper->getObjectIdsForTags( + [$this->tag1->getId(), $this->tag2->getId(), $this->tag3->getId()], + 'testtype', + 1 + ); + } + + public function testGetObjectsForTagsLimitOffset() { + $objectIds = $this->tagMapper->getObjectIdsForTags( + [$this->tag1->getId()], + 'testtype', + 1, + '1' + ); + + $this->assertEquals([ 2, ], $objectIds); } @@ -156,29 +192,29 @@ class SystemTagObjectMapperTest extends TestCase { } public function testAssignUnassignTags() { - $this->tagMapper->unassignTags(1, 'testtype', [$this->tag1->getId()]); + $this->tagMapper->unassignTags('1', 'testtype', [$this->tag1->getId()]); - $tagIdMapping = $this->tagMapper->getTagIdsForObjects(1, 'testtype'); + $tagIdMapping = $this->tagMapper->getTagIdsForObjects('1', 'testtype'); $this->assertEquals([ 1 => [$this->tag2->getId()], ], $tagIdMapping); - $this->tagMapper->assignTags(1, 'testtype', [$this->tag1->getId()]); - $this->tagMapper->assignTags(1, 'testtype', $this->tag3->getId()); + $this->tagMapper->assignTags('1', 'testtype', [$this->tag1->getId()]); + $this->tagMapper->assignTags('1', 'testtype', $this->tag3->getId()); - $tagIdMapping = $this->tagMapper->getTagIdsForObjects(1, 'testtype'); + $tagIdMapping = $this->tagMapper->getTagIdsForObjects('1', 'testtype'); $this->assertEquals([ - 1 => [$this->tag1->getId(), $this->tag2->getId(), $this->tag3->getId()], + '1' => [$this->tag1->getId(), $this->tag2->getId(), $this->tag3->getId()], ], $tagIdMapping); } public function testReAssignUnassignTags() { // reassign tag1 - $this->tagMapper->assignTags(1, 'testtype', [$this->tag1->getId()]); + $this->tagMapper->assignTags('1', 'testtype', [$this->tag1->getId()]); // tag 3 was never assigned - $this->tagMapper->unassignTags(1, 'testtype', [$this->tag3->getId()]); + $this->tagMapper->unassignTags('1', 'testtype', [$this->tag3->getId()]); $this->assertTrue(true, 'No error when reassigning/unassigning'); } @@ -187,13 +223,13 @@ class SystemTagObjectMapperTest extends TestCase { * @expectedException \OCP\SystemTag\TagNotFoundException */ public function testAssignNonExistingTags() { - $this->tagMapper->assignTags(1, 'testtype', [100]); + $this->tagMapper->assignTags('1', 'testtype', [100]); } public function testAssignNonExistingTagInArray() { $caught = false; try { - $this->tagMapper->assignTags(1, 'testtype', [100, $this->tag3->getId()]); + $this->tagMapper->assignTags('1', 'testtype', [100, $this->tag3->getId()]); } catch (TagNotFoundException $e) { $caught = true; } @@ -201,12 +237,12 @@ class SystemTagObjectMapperTest extends TestCase { $this->assertTrue($caught, 'Exception thrown'); $tagIdMapping = $this->tagMapper->getTagIdsForObjects( - [1], + ['1'], 'testtype' ); $this->assertEquals([ - 1 => [$this->tag1->getId(), $this->tag2->getId()], + '1' => [$this->tag1->getId(), $this->tag2->getId()], ], $tagIdMapping, 'None of the tags got assigned'); } @@ -214,13 +250,13 @@ class SystemTagObjectMapperTest extends TestCase { * @expectedException \OCP\SystemTag\TagNotFoundException */ public function testUnassignNonExistingTags() { - $this->tagMapper->unassignTags(1, 'testtype', [100]); + $this->tagMapper->unassignTags('1', 'testtype', [100]); } public function testUnassignNonExistingTagsInArray() { $caught = false; try { - $this->tagMapper->unassignTags(1, 'testtype', [100, $this->tag1->getId()]); + $this->tagMapper->unassignTags('1', 'testtype', [100, $this->tag1->getId()]); } catch (TagNotFoundException $e) { $caught = true; } @@ -233,14 +269,14 @@ class SystemTagObjectMapperTest extends TestCase { ); $this->assertEquals([ - 1 => [$this->tag1->getId(), $this->tag2->getId()], + '1' => [$this->tag1->getId(), $this->tag2->getId()], ], $tagIdMapping, 'None of the tags got unassigned'); } public function testHaveTagAllMatches() { $this->assertTrue( $this->tagMapper->haveTag( - [1], + ['1'], 'testtype', $this->tag1->getId(), true @@ -250,7 +286,7 @@ class SystemTagObjectMapperTest extends TestCase { $this->assertTrue( $this->tagMapper->haveTag( - [1, 2], + ['1', '2'], 'testtype', $this->tag1->getId(), true @@ -260,7 +296,7 @@ class SystemTagObjectMapperTest extends TestCase { $this->assertFalse( $this->tagMapper->haveTag( - [1, 2], + ['1', '2'], 'testtype', $this->tag2->getId(), true @@ -270,7 +306,7 @@ class SystemTagObjectMapperTest extends TestCase { $this->assertFalse( $this->tagMapper->haveTag( - [2], + ['2'], 'testtype', $this->tag2->getId(), true @@ -280,7 +316,7 @@ class SystemTagObjectMapperTest extends TestCase { $this->assertFalse( $this->tagMapper->haveTag( - [3], + ['3'], 'testtype', $this->tag2->getId(), true @@ -292,7 +328,7 @@ class SystemTagObjectMapperTest extends TestCase { public function testHaveTagAtLeastOneMatch() { $this->assertTrue( $this->tagMapper->haveTag( - [1], + ['1'], 'testtype', $this->tag1->getId(), false @@ -302,7 +338,7 @@ class SystemTagObjectMapperTest extends TestCase { $this->assertTrue( $this->tagMapper->haveTag( - [1, 2], + ['1', '2'], 'testtype', $this->tag1->getId(), false @@ -312,7 +348,7 @@ class SystemTagObjectMapperTest extends TestCase { $this->assertTrue( $this->tagMapper->haveTag( - [1, 2], + ['1', '2'], 'testtype', $this->tag2->getId(), false @@ -322,7 +358,7 @@ class SystemTagObjectMapperTest extends TestCase { $this->assertFalse( $this->tagMapper->haveTag( - [2], + ['2'], 'testtype', $this->tag2->getId(), false @@ -332,7 +368,7 @@ class SystemTagObjectMapperTest extends TestCase { $this->assertFalse( $this->tagMapper->haveTag( - [3], + ['3'], 'testtype', $this->tag2->getId(), false @@ -346,7 +382,7 @@ class SystemTagObjectMapperTest extends TestCase { */ public function testHaveTagNonExisting() { $this->tagMapper->haveTag( - [1], + ['1'], 'testtype', 100 ); diff --git a/version.php b/version.php index dfe5d68300c..4ae94717c3d 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ // We only can count up. The 4. digit is only for the internal patchlevel to trigger DB upgrades // between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel // when updating major/minor version number. -$OC_Version = array(9, 0, 0, 12); +$OC_Version = array(9, 0, 0, 13); // The human readable string $OC_VersionString = '9.0.0 beta 2'; |