diff options
32 files changed, 306 insertions, 847 deletions
diff --git a/.drone.yml b/.drone.yml index c54907cc5d9..78cdefd82b4 100644 --- a/.drone.yml +++ b/.drone.yml @@ -148,6 +148,14 @@ pipeline: matrix: DB: postgres PHP: 5.6 + mysqlmb4-php5.6: + image: nextcloudci/php5.6:php5.6-2 + commands: + - NOCOVERAGE=true TEST_SELECTION=DB ./autotest.sh mysqlmb4 + when: + matrix: + DB: mysqlmb4 + PHP: 5.6 integration-capabilities_features: image: nextcloudci/integration-php7.0:integration-php7.0-1 commands: @@ -368,6 +376,8 @@ matrix: PHP: 5.6 - DB: postgres PHP: 5.6 + - DB: mysqlmb4 + PHP: 5.6 services: cache: @@ -390,3 +400,14 @@ services: when: matrix: DB: mysql + mysqlmb4: + image: mysql + environment: + - MYSQL_ROOT_PASSWORD=owncloud + - MYSQL_USER=oc_autotest + - MYSQL_PASSWORD=owncloud + - MYSQL_DATABASE=oc_autotest + command: [ "--innodb_large_prefix=true", "--innodb_file_format=barracuda", "--innodb_file_per_table=true" ] + when: + matrix: + DB: mysqlmb4 diff --git a/apps/files/css/files.css b/apps/files/css/files.css index acd7f4af25d..9b844919c4e 100644 --- a/apps/files/css/files.css +++ b/apps/files/css/files.css @@ -148,10 +148,10 @@ table tr.mouseOver td { } tbody a { color:#000; } -span.extension, span.uploading, td.date { +span.conflict-path, span.extension, span.uploading, td.date { color: #999; } -span.extension { +span.conflict-path, span.extension { -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=70)"; filter: alpha(opacity=70); opacity: .7; @@ -161,6 +161,8 @@ span.extension { transition: opacity 300ms; vertical-align: top; } +tr:hover span.conflict-path, +tr:focus span.conflict-path, tr:hover span.extension, tr:focus span.extension { -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 896af1dd6e5..159d008e6e6 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -1148,6 +1148,28 @@ } var nameSpan=$('<span></span>').addClass('nametext'); var innernameSpan = $('<span></span>').addClass('innernametext').text(basename); + + if (path && path !== '/') { + var conflictingItems = this.$fileList.find('tr[data-file="' + name.replace( /(:|\.|\[|\]|,|=)/g, "\\$1") + '"]'); + if (conflictingItems.length !== 0) { + if (conflictingItems.length === 1) { + // Update the path on the first conflicting item + var $firstConflict = $(conflictingItems[0]), + firstConflictPath = $firstConflict.attr('data-path') + '/'; + if (firstConflictPath.charAt(0) === '/') { + firstConflictPath = firstConflictPath.substr(1); + } + $firstConflict.find('td.filename span.innernametext').prepend($('<span></span>').addClass('conflict-path').text(firstConflictPath)); + } + + var conflictPath = path + '/'; + if (conflictPath.charAt(0) === '/') { + conflictPath = conflictPath.substr(1); + } + nameSpan.append($('<span></span>').addClass('conflict-path').text(conflictPath)); + } + } + nameSpan.append(innernameSpan); linkElem.append(nameSpan); if (extension) { diff --git a/autotest.sh b/autotest.sh index eca3d81c048..61e7426ab99 100755 --- a/autotest.sh +++ b/autotest.sh @@ -21,7 +21,7 @@ ADMINLOGIN=admin$EXECUTOR_NUMBER BASEDIR=$PWD PRIMARY_STORAGE_CONFIGS="local swift" -DBCONFIGS="sqlite mysql mariadb pgsql oci" +DBCONFIGS="sqlite mysql mariadb pgsql oci mysqlmb4" # $PHP_EXE is run through 'which' and as such e.g. 'php' or 'hhvm' is usually # sufficient. Due to the behaviour of 'which', $PHP_EXE may also be a path @@ -209,6 +209,48 @@ function execute_tests { exit 1 fi fi + if [ "$DB" == "mysqlmb4" ] ; then + if [ ! -z "$USEDOCKER" ] ; then + echo "Fire up the mysql docker" + DOCKER_CONTAINER_ID=$(docker run \ + -v $BASEDIR/tests/docker/mysqlmb4:/etc/mysql/conf.d \ + -e MYSQL_ROOT_PASSWORD=owncloud \ + -e MYSQL_USER="$DATABASEUSER" \ + -e MYSQL_PASSWORD=owncloud \ + -e MYSQL_DATABASE="$DATABASENAME" \ + -d mysql:5.7 + --innodb_large_prefix=true + --innodb_file_format=barracuda + --innodb_file_per_table=true) + + DATABASEHOST=$(docker inspect --format="{{.NetworkSettings.IPAddress}}" "$DOCKER_CONTAINER_ID") + + else + if [ -z "$DRONE" ] ; then # no need to drop the DB when we are on CI + if [ "mysql" != "$(mysql --version | grep -o mysql)" ] ; then + echo "Your mysql binary is not provided by mysql" + echo "To use the docker container set the USEDOCKER environment variable" + exit -1 + fi + mysql -u "$DATABASEUSER" -powncloud -e "DROP DATABASE IF EXISTS $DATABASENAME" -h $DATABASEHOST || true + else + DATABASEHOST=127.0.0.1 + fi + fi + + echo "Waiting for MySQL(utf8mb4) initialisation ..." + + if ! apps/files_external/tests/env/wait-for-connection $DATABASEHOST 3306 60; then + echo "[ERROR] Waited 60 seconds, no response" >&2 + exit 1 + fi + sleep 1 + + echo "MySQL(utf8mb4) is up." + _DB="mysql" + + cp tests/docker/mysqlmb4.config.php config + fi if [ "$DB" == "mariadb" ] ; then if [ ! -z "$USEDOCKER" ] ; then echo "Fire up the mariadb docker" diff --git a/config/config.sample.php b/config/config.sample.php index 3aa0f353c59..df1e2d16fc0 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -129,6 +129,7 @@ $CONFIG = array( */ 'dbtableprefix' => '', + /** * Indicates whether the Nextcloud instance was installed successfully; ``true`` * indicates a successful installation, and ``false`` indicates an unsuccessful @@ -1080,6 +1081,34 @@ $CONFIG = array( 'sqlite.journal_mode' => 'DELETE', /** + * If this setting is set to true MySQL can handle 4 byte characters instead of + * 3 byte characters + * + * MySQL requires a special setup for longer indexes (> 767 bytes) which are + * needed: + * + * [mysqld] + * innodb_large_prefix=true + * innodb_file_format=barracuda + * innodb_file_per_table=true + * + * Tables will be created with + * * character set: utf8mb4 + * * collation: utf8mb4_bin + * * row_format: compressed + * + * See: + * https://dev.mysql.com/doc/refman/5.7/en/charset-unicode-utf8mb4.html + * https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_large_prefix + * https://mariadb.com/kb/en/mariadb/xtradbinnodb-server-system-variables/#innodb_large_prefix + * http://www.tocker.ca/2013/10/31/benchmarking-innodb-page-compression-performance.html + * http://mechanics.flite.com/blog/2014/07/29/using-innodb-large-prefix-to-avoid-error-1071/ + * + * WARNING: EXPERIMENTAL + */ +'mysql.utf8mb4' => false, + +/** * Database types that are supported for installation. * * Available: diff --git a/core/css/icons.css b/core/css/icons.css index df36ae58710..d62ab1504db 100644 --- a/core/css/icons.css +++ b/core/css/icons.css @@ -195,6 +195,13 @@ img.icon-loading-small-dark, object.icon-loading-small-dark, video.icon-loading- background-image: url('../img/actions/external.svg?v=1'); } +.icon-fullscreen { + background-image: url('../img/actions/fullscreen.svg?v=1'); +} +.icon-fullscreen-white { + background-image: url('../img/actions/fullscreen-white.svg?v=1'); +} + .icon-history { background-image: url('../img/actions/history.svg?v=1'); } diff --git a/core/css/inputs.css b/core/css/inputs.css index a79f72cfd14..37fedb9a44e 100644 --- a/core/css/inputs.css +++ b/core/css/inputs.css @@ -429,7 +429,7 @@ input:disabled+label, input:disabled:hover+label, input:disabled:focus+label { .primary, input[type="submit"].primary, input[type="button"].primary, button.primary, .button.primary { border: 1px solid #0082c9; background-color: #00a2e9; - color: #ddd; + color: #fff; } .primary:hover, input[type="submit"].primary:hover, input[type="button"].primary:hover, button.primary:hover, .button.primary:hover, .primary:focus, input[type="submit"].primary:focus, input[type="button"].primary:focus, button.primary:focus, .button.primary:focus { diff --git a/core/img/actions/fullscreen-white.svg b/core/img/actions/fullscreen-white.svg new file mode 100644 index 00000000000..77052945fb2 --- /dev/null +++ b/core/img/actions/fullscreen-white.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewBox="0 0 16 16"> + <path d="m8 1c-0.554 0-1 0.446-1 1s0.446 1 1 1h5v5c0 0.554 0.446 1 1 1s1-0.446 1-1v-6c0-0.554-0.446-1-1-1h-6zm-6 6c-0.554 0-1 0.446-1 1v6c0 0.554 0.446 1 1 1h6c0.554 0 1-0.446 1-1s-0.446-1-1-1h-5v-5c0-0.554-0.446-1-1-1z" fill-rule="evenodd" fill="#fff"/> +</svg> diff --git a/core/img/actions/fullscreen.svg b/core/img/actions/fullscreen.svg new file mode 100644 index 00000000000..8bf215779db --- /dev/null +++ b/core/img/actions/fullscreen.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewBox="0 0 16 16"> + <path d="m8 1c-0.554 0-1 0.446-1 1s0.446 1 1 1h5v5c0 0.554 0.446 1 1 1s1-0.446 1-1v-6c0-0.554-0.446-1-1-1h-6zm-6 6c-0.554 0-1 0.446-1 1v6c0 0.554 0.446 1 1 1h6c0.554 0 1-0.446 1-1s-0.446-1-1-1h-5v-5c0-0.554-0.446-1-1-1z" fill-rule="evenodd"/> +</svg> diff --git a/core/js/compatibility.js b/core/js/compatibility.js deleted file mode 100644 index ac942d202e8..00000000000 --- a/core/js/compatibility.js +++ /dev/null @@ -1,155 +0,0 @@ -/** - * implement Object.create for browsers without native support - */ -if (typeof Object.create !== 'function') { - Object.create = function (o) { - function F() {} - F.prototype = o; - return new F(); - }; -} - -/** - * implement Object.keys for browsers without native support - */ -if (typeof Object.keys !== 'function') { - Object.keys = function(o) { - if (o !== Object(o)) { - throw new TypeError('Object.keys called on a non-object'); - } - var k=[],p; - for (p in o) { - if (Object.prototype.hasOwnProperty.call(o,p)) { - k.push(p); - } - } - return k; - }; -} - -/** - * implement Array.filter for browsers without native support - */ -if (!Array.prototype.filter) { - Array.prototype.filter = function(fun /*, thisp*/) { - var len = this.length >>> 0; - if (typeof fun !== "function"){ - throw new TypeError(); - } - - var res = []; - var thisp = arguments[1]; - for (var i = 0; i < len; i++) { - if (i in this) { - var val = this[i]; // in case fun mutates this - if (fun.call(thisp, val, i, this)) { - res.push(val); - } - } - } - return res; - }; -} - -/** - * implement Array.indexOf for browsers without native support - */ -if (!Array.prototype.indexOf){ - Array.prototype.indexOf = function(elt /*, from*/) - { - var len = this.length; - - var from = Number(arguments[1]) || 0; - from = (from < 0) ? Math.ceil(from) : Math.floor(from); - if (from < 0){ - from += len; - } - - for (; from < len; from++) - { - if (from in this && this[from] === elt){ - return from; - } - } - return -1; - }; -} - -/** - * implement Array.map for browsers without native support - */ -if (!Array.prototype.map){ - Array.prototype.map = function(fun /*, thisp */){ - "use strict"; - - if (this === void 0 || this === null){ - throw new TypeError(); - } - - var t = Object(this); - var len = t.length >>> 0; - if (typeof fun !== "function"){ - throw new TypeError(); - } - - var res = new Array(len); - var thisp = arguments[1]; - for (var i = 0; i < len; i++){ - if (i in t){ - res[i] = fun.call(thisp, t[i], i, t); - } - } - - return res; - }; -} - -//https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind -if (!Function.prototype.bind) { - Function.prototype.bind = function (oThis) { - if (typeof this !== "function") { - // closest thing possible to the ECMAScript 5 internal IsCallable function - throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); - } - - var aArgs = Array.prototype.slice.call(arguments, 1), - fToBind = this, - fNOP = function () {}, - fBound = function () { - return fToBind.apply(this instanceof fNOP && oThis - ? this - : oThis, - aArgs.concat(Array.prototype.slice.call(arguments))); - }; - - fNOP.prototype = this.prototype; - fBound.prototype = new fNOP(); - - return fBound; - }; -} - -//https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/Trim -if(!String.prototype.trim) { - String.prototype.trim = function () { - return this.replace(/^\s+|\s+$/g,''); - }; -} - -// Older Firefoxes doesn't support outerHTML -// From http://stackoverflow.com/questions/1700870/how-do-i-do-outerhtml-in-firefox#answer-3819589 -function outerHTML(node){ - // In newer browsers use the internal property otherwise build a wrapper. - return node.outerHTML || ( - function(n){ - var div = document.createElement('div'), h; - div.appendChild( n.cloneNode(true) ); - h = div.innerHTML; - div = null; - return h; - })(node); -} - -// devicePixelRatio for IE10 -window.devicePixelRatio = window.devicePixelRatio || - window.screen.deviceXDPI / window.screen.logicalXDPI || 1; diff --git a/core/js/core.json b/core/js/core.json index e651c9d7597..c98928d0fed 100644 --- a/core/js/core.json +++ b/core/js/core.json @@ -20,7 +20,6 @@ "placeholder.js" ], "modules": [ - "compatibility.js", "jquery.ocdialog.js", "oc-dialogs.js", "js.js", diff --git a/core/js/js.js b/core/js/js.js index 4b09cc49038..16da273c8e1 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -1189,199 +1189,6 @@ OC.Notification={ }; /** - * Breadcrumb class - * - * @namespace - * - * @deprecated will be replaced by the breadcrumb implementation - * of the files app in the future - */ -OC.Breadcrumb={ - container:null, - /** - * @todo Write documentation - * @param dir - * @param leafName - * @param leafLink - */ - show:function(dir, leafName, leafLink){ - if(!this.container){//default - this.container=$('#controls'); - } - this._show(this.container, dir, leafName, leafLink); - }, - _show:function(container, dir, leafname, leaflink){ - var self = this; - - this._clear(container); - - // show home + path in subdirectories - if (dir) { - //add home - var link = OC.linkTo('files','index.php'); - - var crumb=$('<div/>'); - crumb.addClass('crumb'); - - var crumbLink=$('<a/>'); - crumbLink.attr('href',link); - - var crumbImg=$('<img/>'); - crumbImg.attr('src',OC.imagePath('core','places/home')); - crumbLink.append(crumbImg); - crumb.append(crumbLink); - container.prepend(crumb); - - //add path parts - var segments = dir.split('/'); - var pathurl = ''; - jQuery.each(segments, function(i,name) { - if (name !== '') { - pathurl = pathurl+'/'+name; - var link = OC.linkTo('files','index.php')+'?dir='+encodeURIComponent(pathurl); - self._push(container, name, link); - } - }); - } - - //add leafname - if (leafname && leaflink) { - this._push(container, leafname, leaflink); - } - }, - - /** - * @todo Write documentation - * @param {string} name - * @param {string} link - */ - push:function(name, link){ - if(!this.container){//default - this.container=$('#controls'); - } - return this._push(OC.Breadcrumb.container, name, link); - }, - _push:function(container, name, link){ - var crumb=$('<div/>'); - crumb.addClass('crumb').addClass('last'); - - var crumbLink=$('<a/>'); - crumbLink.attr('href',link); - crumbLink.text(name); - crumb.append(crumbLink); - - var existing=container.find('div.crumb'); - if(existing.length){ - existing.removeClass('last'); - existing.last().after(crumb); - }else{ - container.prepend(crumb); - } - return crumb; - }, - - /** - * @todo Write documentation - */ - pop:function(){ - if(!this.container){//default - this.container=$('#controls'); - } - this.container.find('div.crumb').last().remove(); - this.container.find('div.crumb').last().addClass('last'); - }, - - /** - * @todo Write documentation - */ - clear:function(){ - if(!this.container){//default - this.container=$('#controls'); - } - this._clear(this.container); - }, - _clear:function(container) { - container.find('div.crumb').remove(); - } -}; - -if(typeof localStorage !=='undefined' && localStorage !== null){ - /** - * User and instance aware localstorage - * @namespace - */ - OC.localStorage={ - namespace:'oc_'+OC.currentUser+'_'+OC.webroot+'_', - - /** - * Whether the storage contains items - * @param {string} name - * @return {boolean} - */ - hasItem:function(name){ - return OC.localStorage.getItem(name)!==null; - }, - - /** - * Add an item to the storage - * @param {string} name - * @param {string} item - */ - setItem:function(name,item){ - return localStorage.setItem(OC.localStorage.namespace+name,JSON.stringify(item)); - }, - - /** - * Removes an item from the storage - * @param {string} name - * @param {string} item - */ - removeItem:function(name,item){ - return localStorage.removeItem(OC.localStorage.namespace+name); - }, - - /** - * Get an item from the storage - * @param {string} name - * @return {null|string} - */ - getItem:function(name){ - var item = localStorage.getItem(OC.localStorage.namespace+name); - if(item === null) { - return null; - } else { - return JSON.parse(item); - } - } - }; -}else{ - //dummy localstorage - OC.localStorage={ - hasItem:function(){ - return false; - }, - setItem:function(){ - return false; - }, - getItem:function(){ - return null; - } - }; -} - -/** - * prototypical inheritance functions - * @todo Write documentation - * usage: - * MySubObject=object(MyObject) - */ -function object(o) { - function F() {} - F.prototype = o; - return new F(); -} - -/** * Initializes core */ function initCore() { diff --git a/core/js/octemplate.js b/core/js/octemplate.js index 67aa7d69cce..b24ad95c2b0 100644 --- a/core/js/octemplate.js +++ b/core/js/octemplate.js @@ -76,7 +76,7 @@ }, // From stackoverflow.com/questions/1408289/best-way-to-do-variable-interpolation-in-javascript _build: function(o){ - var data = this.elem.attr('type') === 'text/template' ? this.elem.html() : outerHTML(this.elem.get(0)); + var data = this.elem.attr('type') === 'text/template' ? this.elem.html() : this.elem.get(0).outerHTML; try { return data.replace(/{([^{}]*)}/g, function (a, b) { diff --git a/core/js/placeholders.js b/core/js/placeholders.js deleted file mode 100644 index e63f429d40f..00000000000 --- a/core/js/placeholders.js +++ /dev/null @@ -1,459 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2012 James Allardice - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -// Defines the global Placeholders object along with various utility methods -(function (global) { - - "use strict"; - - // Cross-browser DOM event binding - function addEventListener(elem, event, fn) { - if (elem.addEventListener) { - return elem.addEventListener(event, fn, false); - } - if (elem.attachEvent) { - return elem.attachEvent("on" + event, fn); - } - } - - // Check whether an item is in an array (we don't use Array.prototype.indexOf so we don't clobber any existing polyfills - this is a really simple alternative) - function inArray(arr, item) { - var i, len; - for (i = 0, len = arr.length; i < len; i++) { - if (arr[i] === item) { - return true; - } - } - return false; - } - - // Move the caret to the index position specified. Assumes that the element has focus - function moveCaret(elem, index) { - var range; - if (elem.createTextRange) { - range = elem.createTextRange(); - range.move("character", index); - range.select(); - } else if (elem.selectionStart) { - elem.focus(); - elem.setSelectionRange(index, index); - } - } - - // Attempt to change the type property of an input element - function changeType(elem, type) { - try { - elem.type = type; - return true; - } catch (e) { - // You can't change input type in IE8 and below - return false; - } - } - - // Expose public methods - global.Placeholders = { - Utils: { - addEventListener: addEventListener, - inArray: inArray, - moveCaret: moveCaret, - changeType: changeType - } - }; - -}(this)); - -(function (global) { - - "use strict"; - - var validTypes = [ - "text", - "search", - "url", - "tel", - "email", - "password", - "number", - "textarea" - ], - - // The list of keycodes that are not allowed when the polyfill is configured to hide-on-input - badKeys = [ - - // The following keys all cause the caret to jump to the end of the input value - 27, // Escape - 33, // Page up - 34, // Page down - 35, // End - 36, // Home - - // Arrow keys allow you to move the caret manually, which should be prevented when the placeholder is visible - 37, // Left - 38, // Up - 39, // Right - 40, // Down - - // The following keys allow you to modify the placeholder text by removing characters, which should be prevented when the placeholder is visible - 8, // Backspace - 46 // Delete - ], - - // Styling variables - placeholderStyleColor = "#ccc", - placeholderClassName = "placeholdersjs", - classNameRegExp = new RegExp("(?:^|\\s)" + placeholderClassName + "(?!\\S)"), - - // These will hold references to all elements that can be affected. NodeList objects are live, so we only need to get those references once - inputs, textareas, - - // The various data-* attributes used by the polyfill - ATTR_CURRENT_VAL = "data-placeholder-value", - ATTR_ACTIVE = "data-placeholder-active", - ATTR_INPUT_TYPE = "data-placeholder-type", - ATTR_FORM_HANDLED = "data-placeholder-submit", - ATTR_EVENTS_BOUND = "data-placeholder-bound", - ATTR_OPTION_FOCUS = "data-placeholder-focus", - ATTR_OPTION_LIVE = "data-placeholder-live", - ATTR_MAXLENGTH = "data-placeholder-maxlength", - - // Various other variables used throughout the rest of the script - test = document.createElement("input"), - head = document.getElementsByTagName("head")[0], - root = document.documentElement, - Placeholders = global.Placeholders, - Utils = Placeholders.Utils, - hideOnInput, liveUpdates, keydownVal, styleElem, styleRules, placeholder, timer, form, elem, len, i; - - // No-op (used in place of public methods when native support is detected) - function noop() {} - - // Avoid IE9 activeElement of death when an iframe is used. - // More info: - // http://bugs.jquery.com/ticket/13393 - // https://github.com/jquery/jquery/commit/85fc5878b3c6af73f42d61eedf73013e7faae408 - function safeActiveElement() { - try { - return document.activeElement; - } catch (err) {} - } - - // Hide the placeholder value on a single element. Returns true if the placeholder was hidden and false if it was not (because it wasn't visible in the first place) - function hidePlaceholder(elem, keydownValue) { - var type, - maxLength, - valueChanged = (!!keydownValue && elem.value !== keydownValue), - isPlaceholderValue = (elem.value === elem.getAttribute(ATTR_CURRENT_VAL)); - - if ((valueChanged || isPlaceholderValue) && elem.getAttribute(ATTR_ACTIVE) === "true") { - elem.removeAttribute(ATTR_ACTIVE); - elem.value = elem.value.replace(elem.getAttribute(ATTR_CURRENT_VAL), ""); - elem.className = elem.className.replace(classNameRegExp, ""); - - // Restore the maxlength value - maxLength = elem.getAttribute(ATTR_MAXLENGTH); - if (parseInt(maxLength, 10) >= 0) { // Old FF returns -1 if attribute not set (see GH-56) - elem.setAttribute("maxLength", maxLength); - elem.removeAttribute(ATTR_MAXLENGTH); - } - - // If the polyfill has changed the type of the element we need to change it back - type = elem.getAttribute(ATTR_INPUT_TYPE); - if (type) { - elem.type = type; - } - return true; - } - return false; - } - - // Show the placeholder value on a single element. Returns true if the placeholder was shown and false if it was not (because it was already visible) - function showPlaceholder(elem) { - var type, - maxLength, - val = elem.getAttribute(ATTR_CURRENT_VAL); - if (elem.value === "" && val) { - elem.setAttribute(ATTR_ACTIVE, "true"); - elem.value = val; - elem.className += " " + placeholderClassName; - - // Store and remove the maxlength value - maxLength = elem.getAttribute(ATTR_MAXLENGTH); - if (!maxLength) { - elem.setAttribute(ATTR_MAXLENGTH, elem.maxLength); - elem.removeAttribute("maxLength"); - } - - // If the type of element needs to change, change it (e.g. password inputs) - type = elem.getAttribute(ATTR_INPUT_TYPE); - if (type) { - elem.type = "text"; - } else if (elem.type === "password") { - if (Utils.changeType(elem, "text")) { - elem.setAttribute(ATTR_INPUT_TYPE, "password"); - } - } - return true; - } - return false; - } - - function handleElem(node, callback) { - - var handleInputsLength, handleTextareasLength, handleInputs, handleTextareas, elem, len, i; - - // Check if the passed in node is an input/textarea (in which case it can't have any affected descendants) - if (node && node.getAttribute(ATTR_CURRENT_VAL)) { - callback(node); - } else { - - // If an element was passed in, get all affected descendants. Otherwise, get all affected elements in document - handleInputs = node ? node.getElementsByTagName("input") : inputs; - handleTextareas = node ? node.getElementsByTagName("textarea") : textareas; - - handleInputsLength = handleInputs ? handleInputs.length : 0; - handleTextareasLength = handleTextareas ? handleTextareas.length : 0; - - // Run the callback for each element - for (i = 0, len = handleInputsLength + handleTextareasLength; i < len; i++) { - elem = i < handleInputsLength ? handleInputs[i] : handleTextareas[i - handleInputsLength]; - callback(elem); - } - } - } - - // Return all affected elements to their normal state (remove placeholder value if present) - function disablePlaceholders(node) { - handleElem(node, hidePlaceholder); - } - - // Show the placeholder value on all appropriate elements - function enablePlaceholders(node) { - handleElem(node, showPlaceholder); - } - - // Returns a function that is used as a focus event handler - function makeFocusHandler(elem) { - return function () { - - // Only hide the placeholder value if the (default) hide-on-focus behaviour is enabled - if (hideOnInput && elem.value === elem.getAttribute(ATTR_CURRENT_VAL) && elem.getAttribute(ATTR_ACTIVE) === "true") { - - // Move the caret to the start of the input (this mimics the behaviour of all browsers that do not hide the placeholder on focus) - Utils.moveCaret(elem, 0); - - } else { - - // Remove the placeholder - hidePlaceholder(elem); - } - }; - } - - // Returns a function that is used as a blur event handler - function makeBlurHandler(elem) { - return function () { - showPlaceholder(elem); - }; - } - - // Functions that are used as a event handlers when the hide-on-input behaviour has been activated - very basic implementation of the "input" event - function makeKeydownHandler(elem) { - return function (e) { - keydownVal = elem.value; - - //Prevent the use of the arrow keys (try to keep the cursor before the placeholder) - if (elem.getAttribute(ATTR_ACTIVE) === "true") { - if (keydownVal === elem.getAttribute(ATTR_CURRENT_VAL) && Utils.inArray(badKeys, e.keyCode)) { - if (e.preventDefault) { - e.preventDefault(); - } - return false; - } - } - }; - } - function makeKeyupHandler(elem) { - return function () { - hidePlaceholder(elem, keydownVal); - - // If the element is now empty we need to show the placeholder - if (elem.value === "") { - elem.blur(); - Utils.moveCaret(elem, 0); - } - }; - } - function makeClickHandler(elem) { - return function () { - if (elem === safeActiveElement() && elem.value === elem.getAttribute(ATTR_CURRENT_VAL) && elem.getAttribute(ATTR_ACTIVE) === "true") { - Utils.moveCaret(elem, 0); - } - }; - } - - // Returns a function that is used as a submit event handler on form elements that have children affected by this polyfill - function makeSubmitHandler(form) { - return function () { - - // Turn off placeholders on all appropriate descendant elements - disablePlaceholders(form); - }; - } - - // Bind event handlers to an element that we need to affect with the polyfill - function newElement(elem) { - - // If the element is part of a form, make sure the placeholder string is not submitted as a value - if (elem.form) { - form = elem.form; - - // If the type of the property is a string then we have a "form" attribute and need to get the real form - if (typeof form === "string") { - form = document.getElementById(form); - } - - // Set a flag on the form so we know it's been handled (forms can contain multiple inputs) - if (!form.getAttribute(ATTR_FORM_HANDLED)) { - Utils.addEventListener(form, "submit", makeSubmitHandler(form)); - form.setAttribute(ATTR_FORM_HANDLED, "true"); - } - } - - // Bind event handlers to the element so we can hide/show the placeholder as appropriate - Utils.addEventListener(elem, "focus", makeFocusHandler(elem)); - Utils.addEventListener(elem, "blur", makeBlurHandler(elem)); - - // If the placeholder should hide on input rather than on focus we need additional event handlers - if (hideOnInput) { - Utils.addEventListener(elem, "keydown", makeKeydownHandler(elem)); - Utils.addEventListener(elem, "keyup", makeKeyupHandler(elem)); - Utils.addEventListener(elem, "click", makeClickHandler(elem)); - } - - // Remember that we've bound event handlers to this element - elem.setAttribute(ATTR_EVENTS_BOUND, "true"); - elem.setAttribute(ATTR_CURRENT_VAL, placeholder); - - // If the element doesn't have a value and is not focussed, set it to the placeholder string - if (hideOnInput || elem !== safeActiveElement()) { - showPlaceholder(elem); - } - } - - Placeholders.nativeSupport = test.placeholder !== void 0; - - if (!Placeholders.nativeSupport) { - - // Get references to all the input and textarea elements currently in the DOM (live NodeList objects to we only need to do this once) - inputs = document.getElementsByTagName("input"); - textareas = document.getElementsByTagName("textarea"); - - // Get any settings declared as data-* attributes on the root element (currently the only options are whether to hide the placeholder on focus or input and whether to auto-update) - hideOnInput = root.getAttribute(ATTR_OPTION_FOCUS) === "false"; - liveUpdates = root.getAttribute(ATTR_OPTION_LIVE) !== "false"; - - // Create style element for placeholder styles (instead of directly setting style properties on elements - allows for better flexibility alongside user-defined styles) - styleElem = document.createElement("style"); - styleElem.type = "text/css"; - - // Create style rules as text node - styleRules = document.createTextNode("." + placeholderClassName + " { color:" + placeholderStyleColor + "; }"); - - // Append style rules to newly created stylesheet - if (styleElem.styleSheet) { - styleElem.styleSheet.cssText = styleRules.nodeValue; - } else { - styleElem.appendChild(styleRules); - } - - // Prepend new style element to the head (before any existing stylesheets, so user-defined rules take precedence) - head.insertBefore(styleElem, head.firstChild); - - // Set up the placeholders - for (i = 0, len = inputs.length + textareas.length; i < len; i++) { - elem = i < inputs.length ? inputs[i] : textareas[i - inputs.length]; - - // Get the value of the placeholder attribute, if any. IE10 emulating IE7 fails with getAttribute, hence the use of the attributes node - placeholder = elem.attributes.placeholder; - if (placeholder) { - - // IE returns an empty object instead of undefined if the attribute is not present - placeholder = placeholder.nodeValue; - - // Only apply the polyfill if this element is of a type that supports placeholders, and has a placeholder attribute with a non-empty value - if (placeholder && Utils.inArray(validTypes, elem.type)) { - newElement(elem); - } - } - } - - // If enabled, the polyfill will repeatedly check for changed/added elements and apply to those as well - timer = setInterval(function () { - for (i = 0, len = inputs.length + textareas.length; i < len; i++) { - elem = i < inputs.length ? inputs[i] : textareas[i - inputs.length]; - - // Only apply the polyfill if this element is of a type that supports placeholders, and has a placeholder attribute with a non-empty value - placeholder = elem.attributes.placeholder; - if (placeholder) { - placeholder = placeholder.nodeValue; - if (placeholder && Utils.inArray(validTypes, elem.type)) { - - // If the element hasn't had event handlers bound to it then add them - if (!elem.getAttribute(ATTR_EVENTS_BOUND)) { - newElement(elem); - } - - // If the placeholder value has changed or not been initialised yet we need to update the display - if (placeholder !== elem.getAttribute(ATTR_CURRENT_VAL) || (elem.type === "password" && !elem.getAttribute(ATTR_INPUT_TYPE))) { - - // Attempt to change the type of password inputs (fails in IE < 9) - if (elem.type === "password" && !elem.getAttribute(ATTR_INPUT_TYPE) && Utils.changeType(elem, "text")) { - elem.setAttribute(ATTR_INPUT_TYPE, "password"); - } - - // If the placeholder value has changed and the placeholder is currently on display we need to change it - if (elem.value === elem.getAttribute(ATTR_CURRENT_VAL)) { - elem.value = placeholder; - } - - // Keep a reference to the current placeholder value in case it changes via another script - elem.setAttribute(ATTR_CURRENT_VAL, placeholder); - } - } - } else if (elem.getAttribute(ATTR_ACTIVE)) { - hidePlaceholder(elem); - elem.removeAttribute(ATTR_CURRENT_VAL); - } - } - - // If live updates are not enabled cancel the timer - if (!liveUpdates) { - clearInterval(timer); - } - }, 100); - } - - Utils.addEventListener(global, "beforeunload", function () { - Placeholders.disable(); - }); - - // Expose public methods - Placeholders.disable = Placeholders.nativeSupport ? noop : disablePlaceholders; - Placeholders.enable = Placeholders.nativeSupport ? noop : enablePlaceholders; - -}(this)); diff --git a/core/register_command.php b/core/register_command.php index a6da3cbd899..89b0cf31ef4 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -83,7 +83,7 @@ if (\OC::$server->getConfig()->getSystemValue('installed', false)) { $application->add(new OC\Core\Command\Config\System\SetConfig(\OC::$server->getSystemConfig())); $application->add(new OC\Core\Command\Db\GenerateChangeScript()); - $application->add(new OC\Core\Command\Db\ConvertType(\OC::$server->getConfig(), new \OC\DB\ConnectionFactory())); + $application->add(new OC\Core\Command\Db\ConvertType(\OC::$server->getConfig(), new \OC\DB\ConnectionFactory(\OC::$server->getConfig()))); $application->add(new OC\Core\Command\Encryption\Disable(\OC::$server->getConfig())); $application->add(new OC\Core\Command\Encryption\Enable(\OC::$server->getConfig(), \OC::$server->getEncryptionManager())); diff --git a/lib/private/DB/AdapterMySQL.php b/lib/private/DB/AdapterMySQL.php index 3e2fceda8db..aa784bb83dc 100644 --- a/lib/private/DB/AdapterMySQL.php +++ b/lib/private/DB/AdapterMySQL.php @@ -27,6 +27,9 @@ namespace OC\DB; class AdapterMySQL extends Adapter { + /** @var string */ + protected $charset; + /** * @param string $tableName */ @@ -39,7 +42,16 @@ class AdapterMySQL extends Adapter { } public function fixupStatement($statement) { - $statement = str_replace(' ILIKE ', ' COLLATE utf8_general_ci LIKE ', $statement); + $statement = str_replace(' ILIKE ', ' COLLATE ' . $this->getCharset() . '_general_ci LIKE ', $statement); return $statement; } + + protected function getCharset() { + if (!$this->charset) { + $params = $this->conn->getParams(); + $this->charset = isset($params['charset']) ? $params['charset'] : 'utf8'; + } + + return $this->charset; + } } diff --git a/lib/private/DB/ConnectionFactory.php b/lib/private/DB/ConnectionFactory.php index b2c356edef7..adb180da0c0 100644 --- a/lib/private/DB/ConnectionFactory.php +++ b/lib/private/DB/ConnectionFactory.php @@ -28,6 +28,7 @@ namespace OC\DB; use Doctrine\DBAL\Event\Listeners\OracleSessionInit; use Doctrine\DBAL\Event\Listeners\SQLSessionInit; use Doctrine\DBAL\Event\Listeners\MysqlSessionInit; +use OCP\IConfig; /** * Takes care of creating and configuring Doctrine connections. @@ -64,6 +65,12 @@ class ConnectionFactory { ), ); + public function __construct(IConfig $config) { + if($config->getSystemValue('mysql.utf8mb4', false)) { + $this->defaultConnectionParams['mysql']['charset'] = 'utf8mb4'; + } + } + /** * @brief Get default connection parameters for a given DBMS. * @param string $type DBMS type @@ -99,7 +106,9 @@ class ConnectionFactory { case 'mysql': // Send "SET NAMES utf8". Only required on PHP 5.3 below 5.3.6. // See http://stackoverflow.com/questions/4361459/php-pdo-charset-set-names#4361485 - $eventManager->addEventSubscriber(new MysqlSessionInit); + $eventManager->addEventSubscriber(new MysqlSessionInit( + $this->defaultConnectionParams['mysql']['charset'] + )); $eventManager->addEventSubscriber( new SQLSessionInit("SET SESSION AUTOCOMMIT=1")); break; diff --git a/lib/private/DB/MDB2SchemaReader.php b/lib/private/DB/MDB2SchemaReader.php index 3f183c9723a..c198bb31e00 100644 --- a/lib/private/DB/MDB2SchemaReader.php +++ b/lib/private/DB/MDB2SchemaReader.php @@ -33,6 +33,7 @@ namespace OC\DB; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Schema\SchemaConfig; +use Doctrine\DBAL\Platforms\MySqlPlatform; use OCP\IConfig; class MDB2SchemaReader { @@ -54,12 +55,16 @@ class MDB2SchemaReader { /** @var \Doctrine\DBAL\Schema\SchemaConfig $schemaConfig */ protected $schemaConfig; + /** @var IConfig */ + protected $config; + /** * @param \OCP\IConfig $config * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform */ public function __construct(IConfig $config, AbstractPlatform $platform) { $this->platform = $platform; + $this->config = $config; $this->DBNAME = $config->getSystemValue('dbname', 'owncloud'); $this->DBTABLEPREFIX = $config->getSystemValue('dbtableprefix', 'oc_'); @@ -118,8 +123,15 @@ class MDB2SchemaReader { $name = str_replace('*dbprefix*', $this->DBTABLEPREFIX, $name); $name = $this->platform->quoteIdentifier($name); $table = $schema->createTable($name); - $table->addOption('collate', 'utf8_bin'); $table->setSchemaConfig($this->schemaConfig); + + if($this->platform instanceof MySqlPlatform && $this->config->getSystemValue('mysql.utf8mb4', false)) { + $table->addOption('charset', 'utf8mb4'); + $table->addOption('collate', 'utf8mb4_bin'); + $table->addOption('row_format', 'compressed'); + } else { + $table->addOption('collate', 'utf8_bin'); + } break; case 'create': case 'overwrite': diff --git a/lib/private/DB/MDB2SchemaWriter.php b/lib/private/DB/MDB2SchemaWriter.php index 26e32b036fe..7664b4359ab 100644 --- a/lib/private/DB/MDB2SchemaWriter.php +++ b/lib/private/DB/MDB2SchemaWriter.php @@ -43,7 +43,11 @@ class MDB2SchemaWriter { $xml->addChild('name', $config->getSystemValue('dbname', 'owncloud')); $xml->addChild('create', 'true'); $xml->addChild('overwrite', 'false'); - $xml->addChild('charset', 'utf8'); + if($config->getSystemValue('dbtype', 'sqlite') === 'mysql' && $config->getSystemValue('mysql.utf8mb4', false)) { + $xml->addChild('charset', 'utf8mb4'); + } else { + $xml->addChild('charset', 'utf8'); + } // FIX ME: bloody work around if ($config->getSystemValue('dbtype', 'sqlite') === 'oci') { diff --git a/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php b/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php index 66d8851632f..17f7fd5aa47 100644 --- a/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php +++ b/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php @@ -24,18 +24,31 @@ namespace OC\DB\QueryBuilder\ExpressionBuilder; -use OC\DB\QueryBuilder\QueryFunction; -use OCP\DB\QueryBuilder\IQueryBuilder; +use OC\DB\Connection; +use OCP\IDBConnection; class MySqlExpressionBuilder extends ExpressionBuilder { + /** @var string */ + protected $charset; + + /** + * @param \OCP\IDBConnection|Connection $connection + */ + public function __construct(IDBConnection $connection) { + parent::__construct($connection); + + $params = $connection->getParams(); + $this->charset = isset($params['charset']) ? $params['charset'] : 'utf8'; + } + /** * @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); + return $this->expressionBuilder->comparison($x, ' COLLATE ' . $this->charset . '_general_ci LIKE', $y); } } diff --git a/lib/private/Repair.php b/lib/private/Repair.php index 2ba118b9c37..7a5ef9fbd9e 100644 --- a/lib/private/Repair.php +++ b/lib/private/Repair.php @@ -129,6 +129,7 @@ class Repair implements IOutput{ */ public static function getRepairSteps() { return [ + new Collation(\OC::$server->getConfig(), \OC::$server->getLogger(), \OC::$server->getDatabaseConnection(), false), new RepairMimeTypes(\OC::$server->getConfig()), new RepairLegacyStorages(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection()), new AssetCache(), @@ -179,7 +180,7 @@ class Repair implements IOutput{ $connection = \OC::$server->getDatabaseConnection(); $steps = [ new InnoDB(), - new Collation(\OC::$server->getConfig(), $connection), + new Collation(\OC::$server->getConfig(), \OC::$server->getLogger(), $connection, true), new SqliteAutoincrement($connection), new SearchLuceneTables(), ]; diff --git a/lib/private/Repair/Collation.php b/lib/private/Repair/Collation.php index 74c4ca2e6ac..54de1a719bd 100644 --- a/lib/private/Repair/Collation.php +++ b/lib/private/Repair/Collation.php @@ -24,28 +24,38 @@ namespace OC\Repair; +use Doctrine\DBAL\Exception\DriverException; use Doctrine\DBAL\Platforms\MySqlPlatform; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\ILogger; use OCP\Migration\IOutput; use OCP\Migration\IRepairStep; class Collation implements IRepairStep { - /** - * @var \OCP\IConfig - */ + /** @var IConfig */ protected $config; - /** - * @var \OC\DB\Connection - */ + /** @var ILogger */ + protected $logger; + + /** @var IDBConnection */ protected $connection; + /** @var bool */ + protected $ignoreFailures; + /** - * @param \OCP\IConfig $config - * @param \OC\DB\Connection $connection + * @param IConfig $config + * @param ILogger $logger + * @param IDBConnection $connection + * @param bool $ignoreFailures */ - public function __construct($config, $connection) { + public function __construct(IConfig $config, ILogger $logger, IDBConnection $connection, $ignoreFailures) { $this->connection = $connection; $this->config = $config; + $this->logger = $logger; + $this->ignoreFailures = $ignoreFailures; } public function getName() { @@ -61,11 +71,21 @@ class Collation implements IRepairStep { return; } + $characterSet = $this->config->getSystemValue('mysql.utf8mb4', false) ? 'utf8mb4' : 'utf8'; + $tables = $this->getAllNonUTF8BinTables($this->connection); foreach ($tables as $table) { $output->info("Change collation for $table ..."); - $query = $this->connection->prepare('ALTER TABLE `' . $table . '` CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin;'); - $query->execute(); + $query = $this->connection->prepare('ALTER TABLE `' . $table . '` CONVERT TO CHARACTER SET ' . $characterSet . ' COLLATE ' . $characterSet . '_bin;'); + try { + $query->execute(); + } catch (DriverException $e) { + // Just log this + $this->logger->logException($e); + if (!$this->ignoreFailures) { + throw $e; + } + } } } @@ -75,11 +95,12 @@ class Collation implements IRepairStep { */ protected function getAllNonUTF8BinTables($connection) { $dbName = $this->config->getSystemValue("dbname"); + $characterSet = $this->config->getSystemValue('mysql.utf8mb4', false) ? 'utf8mb4' : 'utf8'; $rows = $connection->fetchAll( "SELECT DISTINCT(TABLE_NAME) AS `table`" . " FROM INFORMATION_SCHEMA . COLUMNS" . " WHERE TABLE_SCHEMA = ?" . - " AND (COLLATION_NAME <> 'utf8_bin' OR CHARACTER_SET_NAME <> 'utf8')" . + " AND (COLLATION_NAME <> '" . $characterSet . "_bin' OR CHARACTER_SET_NAME <> '" . $characterSet . "')" . " AND TABLE_NAME LIKE \"*PREFIX*%\"", array($dbName) ); diff --git a/lib/private/Server.php b/lib/private/Server.php index 291714b23d1..11558118d52 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -407,8 +407,8 @@ class Server extends ServerContainer implements IServerContainer { return new CredentialsManager($c->getCrypto(), $c->getDatabaseConnection()); }); $this->registerService('DatabaseConnection', function (Server $c) { - $factory = new \OC\DB\ConnectionFactory(); $systemConfig = $c->getSystemConfig(); + $factory = new \OC\DB\ConnectionFactory($c->getConfig()); $type = $systemConfig->getValue('dbtype', 'sqlite'); if (!$factory->isValidType($type)) { throw new \OC\DatabaseException('Invalid database type'); diff --git a/lib/private/Setup/AbstractDatabase.php b/lib/private/Setup/AbstractDatabase.php index 310f74d4c0c..47c3e5ee1c6 100644 --- a/lib/private/Setup/AbstractDatabase.php +++ b/lib/private/Setup/AbstractDatabase.php @@ -134,7 +134,7 @@ abstract class AbstractDatabase { } $connectionParams = array_merge($connectionParams, $configOverwrite); - $cf = new ConnectionFactory(); + $cf = new ConnectionFactory($this->config); return $cf->getConnection($this->config->getSystemValue('dbtype', 'sqlite'), $connectionParams); } diff --git a/lib/private/Setup/MySQL.php b/lib/private/Setup/MySQL.php index 4ad6926c2d7..c022616d8b3 100644 --- a/lib/private/Setup/MySQL.php +++ b/lib/private/Setup/MySQL.php @@ -58,8 +58,9 @@ class MySQL extends AbstractDatabase { try{ $name = $this->dbName; $user = $this->dbUser; - //we can't use OC_BD functions here because we need to connect as the administrative user. - $query = "CREATE DATABASE IF NOT EXISTS `$name` CHARACTER SET utf8 COLLATE utf8_bin;"; + //we can't use OC_DB functions here because we need to connect as the administrative user. + $characterSet = \OC::$server->getSystemConfig()->getValue('mysql.utf8mb4', false) ? 'utf8mb4' : 'utf8'; + $query = "CREATE DATABASE IF NOT EXISTS `$name` CHARACTER SET $characterSet COLLATE ${characterSet}_bin;"; $connection->executeUpdate($query); } catch (\Exception $ex) { $this->logger->error('Database creation failed: {error}', [ diff --git a/lib/private/legacy/template.php b/lib/private/legacy/template.php index 477fd624a9d..b43b4da839a 100644 --- a/lib/private/legacy/template.php +++ b/lib/private/legacy/template.php @@ -143,8 +143,6 @@ class OC_Template extends \OC\Template\Base { OC_Util::addScript("oc-dialogs", null, true); OC_Util::addScript("jquery.ocdialog", null, true); OC_Util::addStyle("jquery.ocdialog"); - OC_Util::addScript("compatibility", null, true); - OC_Util::addScript("placeholders", null, true); OC_Util::addScript('files/fileinfo'); OC_Util::addScript('files/client'); diff --git a/tests/data/db_structure.xml b/tests/data/db_structure.xml index 371da944832..b155114f2ed 100644 --- a/tests/data/db_structure.xml +++ b/tests/data/db_structure.xml @@ -293,4 +293,19 @@ </table> + <table> + + <name>*dbprefix*text_table</name> + <declaration> + + <field> + <name>textfield</name> + <type>text</type> + <notnull>false</notnull> + <length>255</length> + </field> + + </declaration> + </table> + </database> diff --git a/tests/docker/mysqlmb4.config.php b/tests/docker/mysqlmb4.config.php new file mode 100644 index 00000000000..4e78272fab4 --- /dev/null +++ b/tests/docker/mysqlmb4.config.php @@ -0,0 +1,5 @@ +<?php + +$CONFIG = array( + 'mysql.utf8mb4' => true, +); diff --git a/tests/docker/mysqlmb4/mb4.cnf b/tests/docker/mysqlmb4/mb4.cnf new file mode 100644 index 00000000000..00333eab10d --- /dev/null +++ b/tests/docker/mysqlmb4/mb4.cnf @@ -0,0 +1,5 @@ + +[mysqld] +innodb_large_prefix=true +innodb_file_format=barracuda +innodb_file_per_table=true diff --git a/tests/lib/DB/LegacyDBTest.php b/tests/lib/DB/LegacyDBTest.php index 7aeeb3dd1f9..f3de570c52d 100644 --- a/tests/lib/DB/LegacyDBTest.php +++ b/tests/lib/DB/LegacyDBTest.php @@ -46,6 +46,11 @@ class LegacyDBTest extends \Test\TestCase { */ private $table5; + /** + * @var string + */ + private $text_table; + protected function setUp() { parent::setUp(); @@ -63,6 +68,7 @@ class LegacyDBTest extends \Test\TestCase { $this->table3 = $this->test_prefix.'vcategory'; $this->table4 = $this->test_prefix.'decimal'; $this->table5 = $this->test_prefix.'uniconst'; + $this->text_table = $this->test_prefix.'text_table'; } protected function tearDown() { @@ -390,4 +396,33 @@ class LegacyDBTest extends \Test\TestCase { $result = $query->execute(array('%ba%')); $this->assertCount(1, $result->fetchAll()); } + + /** + * @dataProvider insertAndSelectDataProvider + */ + public function testInsertAndSelectData($expected, $throwsOnMysqlWithoutUTF8MB4) { + $table = "*PREFIX*{$this->text_table}"; + $config = \OC::$server->getConfig(); + + $query = OC_DB::prepare("INSERT INTO `$table` (`textfield`) VALUES (?)"); + if ($throwsOnMysqlWithoutUTF8MB4 && $config->getSystemValue('dbtype', 'sqlite') === 'mysql' && $config->getSystemValue('mysql.utf8mb4', false) === false) { + $this->markTestSkipped('MySQL requires UTF8mb4 to store value: ' . $expected); + } + $result = $query->execute(array($expected)); + $this->assertEquals(1, $result); + + $actual = OC_DB::prepare("SELECT `textfield` FROM `$table`")->execute()->fetchOne(); + $this->assertSame($expected, $actual); + } + + public function insertAndSelectDataProvider() { + return [ + ['abcdefghijklmnopqrstuvwxyzABCDEFGHIKLMNOPQRSTUVWXYZ', false], + ['0123456789', false], + ['äöüÄÖÜß!"§$%&/()=?#\'+*~°^`´', false], + ['²³¼½¬{[]}\\', false], + ['♡⚗', false], + ['💩', true], # :hankey: on github + ]; + } } diff --git a/tests/lib/Files/Cache/CacheTest.php b/tests/lib/Files/Cache/CacheTest.php index 4a2581fbc54..4c4f43d63d5 100644 --- a/tests/lib/Files/Cache/CacheTest.php +++ b/tests/lib/Files/Cache/CacheTest.php @@ -113,7 +113,8 @@ class CacheTest extends \Test\TestCase { public function testFolder($folder) { if(strpos($folder, 'F09F9890')) { // 4 byte UTF doesn't work on mysql - if(\OC::$server->getDatabaseConnection()->getDatabasePlatform() instanceof MySqlPlatform) { + $params = \OC::$server->getDatabaseConnection()->getParams(); + if(\OC::$server->getDatabaseConnection()->getDatabasePlatform() instanceof MySqlPlatform && $params['charset'] !== 'utf8mb4') { $this->markTestSkipped('MySQL doesn\'t support 4 byte UTF-8'); } } diff --git a/tests/lib/Repair/RepairCollationTest.php b/tests/lib/Repair/RepairCollationTest.php index 2e304a74abc..897f772a794 100644 --- a/tests/lib/Repair/RepairCollationTest.php +++ b/tests/lib/Repair/RepairCollationTest.php @@ -1,9 +1,4 @@ <?php - -namespace Test\Repair; - -use OCP\Migration\IOutput; - /** * Copyright (c) 2014 Thomas Müller <deepdiver@owncloud.com> * This file is licensed under the Affero General Public License version 3 or @@ -11,6 +6,11 @@ use OCP\Migration\IOutput; * See the COPYING-README file. */ +namespace Test\Repair; + +use OCP\ILogger; +use OCP\Migration\IOutput; + class TestCollationRepair extends \OC\Repair\Collation { /** * @param \Doctrine\DBAL\Connection $connection @@ -50,10 +50,14 @@ class RepairCollationTest extends \Test\TestCase { */ private $config; + /** @var ILogger */ + private $logger; + protected function setUp() { parent::setUp(); $this->connection = \OC::$server->getDatabaseConnection(); + $this->logger = $this->createMock(ILogger::class); $this->config = \OC::$server->getConfig(); if (!$this->connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\MySqlPlatform) { $this->markTestSkipped("Test only relevant on MySql"); @@ -63,7 +67,7 @@ class RepairCollationTest extends \Test\TestCase { $this->tableName = $this->getUniqueID($dbPrefix . "_collation_test"); $this->connection->exec("CREATE TABLE $this->tableName(text VARCHAR(16)) COLLATE utf8_unicode_ci"); - $this->repair = new TestCollationRepair($this->config, $this->connection); + $this->repair = new TestCollationRepair($this->config, $this->logger, $this->connection, false); } protected function tearDown() { |