diff options
98 files changed, 2170 insertions, 733 deletions
diff --git a/.gitignore b/.gitignore index 8c8b61d701b..25cb1b227f9 100644 --- a/.gitignore +++ b/.gitignore @@ -88,6 +88,7 @@ nbproject # Tests - auto-generated files /data-autotest /tests/coverage* +/tests/karma-coverage /tests/autoconfig* /tests/autotest* /tests/data/lorem-copy.txt diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 00000000000..f40dd22b5fd --- /dev/null +++ b/.jshintrc @@ -0,0 +1,28 @@ +{ + "camelCase": true, + "eqeqeq": true, + "immed": true, + "latedef": false, + "noarg": true, + "nonbsp": true, + "undef": true, + "unused": true, + "trailing": true, + "maxparams": 5, + "curly": true, + "jquery": true, + "maxlen": 80, + "indent": 4, + "browser": true, + "globals": { + "console": true, + "it": true, + "itx": true, + "expect": true, + "describe": true, + "beforeEach": true, + "afterEach": true, + "sinon": true, + "fakeServer": true + } +} diff --git a/README.md b/README.md index ca7b04a925a..3f76c1a4773 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ A personal cloud which runs on your own server. ### Build Status on [Jenkins CI](https://ci.owncloud.org/) -Git master: [![Build Status](https://ci.owncloud.org/buildStatus/icon?job=ownCloud-Server%28master%29)](https://ci.owncloud.org/job/ownCloud-Server%28master%29/) +Git master: [![Build Status](https://ci.owncloud.org/job/server-master-linux/badge/icon)](https://ci.owncloud.org/job/server-master-linux/) + +Quality: [![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/owncloud/core/badges/quality-score.png?s=ce2f5ded03d4ac628e9ee5c767243fa7412e644f)](https://scrutinizer-ci.com/g/owncloud/core/) ### Installation instructions http://doc.owncloud.org/server/5.0/developer_manual/app/gettingstarted.html diff --git a/apps/files/ajax/newfile.php b/apps/files/ajax/newfile.php index ec5b716fb2a..1853098c507 100644 --- a/apps/files/ajax/newfile.php +++ b/apps/files/ajax/newfile.php @@ -64,6 +64,15 @@ if(strpos($filename, '/') !== false) { exit(); } +if (!\OC\Files\Filesystem::file_exists($dir . '/')) { + $result['data'] = array('message' => (string)$l10n->t( + 'The target folder has been moved or deleted.'), + 'code' => 'targetnotfound' + ); + OCP\JSON::error($result); + exit(); +} + //TODO why is stripslashes used on foldername in newfolder.php but not here? $target = $dir.'/'.$filename; diff --git a/apps/files/ajax/newfolder.php b/apps/files/ajax/newfolder.php index 2cbc8cfeba5..4cfcae3090d 100644 --- a/apps/files/ajax/newfolder.php +++ b/apps/files/ajax/newfolder.php @@ -29,6 +29,15 @@ if(strpos($foldername, '/') !== false) { exit(); } +if (!\OC\Files\Filesystem::file_exists($dir . '/')) { + $result['data'] = array('message' => (string)$l10n->t( + 'The target folder has been moved or deleted.'), + 'code' => 'targetnotfound' + ); + OCP\JSON::error($result); + exit(); +} + //TODO why is stripslashes used on foldername here but not in newfile.php? $target = $dir . '/' . stripslashes($foldername); diff --git a/apps/files/ajax/upload.php b/apps/files/ajax/upload.php index bdaf6a77d14..8f6c42d6620 100644 --- a/apps/files/ajax/upload.php +++ b/apps/files/ajax/upload.php @@ -8,6 +8,7 @@ OCP\JSON::setContentTypeHeader('text/plain'); // If no token is sent along, rely on login only $allowedPermissions = OCP\PERMISSION_ALL; +$errorCode = null; $l = OC_L10N::get('files'); if (empty($_POST['dirToken'])) { @@ -125,7 +126,8 @@ if (strpos($dir, '..') === false) { $meta = \OC\Files\Filesystem::getFileInfo($target); if ($meta === false) { - $error = $l->t('Upload failed. Could not get file info.'); + $error = $l->t('The target folder has been moved or deleted.'); + $errorCode = 'targetnotfound'; } else { $result[] = array('status' => 'success', 'mime' => $meta['mimetype'], @@ -177,5 +179,5 @@ if ($error === false) { OCP\JSON::encodedPrint($result); exit(); } else { - OCP\JSON::error(array(array('data' => array_merge(array('message' => $error), $storageStats)))); + OCP\JSON::error(array(array('data' => array_merge(array('message' => $error, 'code' => $errorCode), $storageStats)))); } diff --git a/apps/files/appinfo/remote.php b/apps/files/appinfo/remote.php index 9f290796205..ef22fe92188 100644 --- a/apps/files/appinfo/remote.php +++ b/apps/files/appinfo/remote.php @@ -52,6 +52,7 @@ $server->addPlugin(new OC_Connector_Sabre_FilesPlugin()); $server->addPlugin(new OC_Connector_Sabre_AbortedUploadDetectionPlugin()); $server->addPlugin(new OC_Connector_Sabre_QuotaPlugin()); $server->addPlugin(new OC_Connector_Sabre_MaintenancePlugin()); +$server->addPlugin(new OC_Connector_Sabre_ExceptionLoggerPlugin('webdav')); // And off we go! $server->exec(); diff --git a/apps/files/css/files.css b/apps/files/css/files.css index 16ee2b9bca0..5526abaf6e2 100644 --- a/apps/files/css/files.css +++ b/apps/files/css/files.css @@ -65,10 +65,15 @@ top: 44px; width: 100%; } -#filestable tbody tr { background-color:#fff; height:40px; } -#filestable, #controls { - min-width: 680px; +/* make sure there's enough room for the file actions */ +#body-user #filestable { + min-width: 750px; +} +#body-user #controls { + min-width: 600px; } + +#filestable tbody tr { background-color:#fff; height:40px; } #filestable tbody tr:hover, tbody tr:active { background-color: rgb(240,240,240); } @@ -98,7 +103,7 @@ table td { } table th#headerName { position: relative; - width: 100em; /* not really sure why this works better than 100% … table styling */ + width: 9999px; /* not really sure why this works better than 100% … table styling */ padding: 0; } #headerName-container { @@ -114,7 +119,9 @@ table th#headerDate, table td.date { -moz-box-sizing: border-box; box-sizing: border-box; position: relative; + /* this can not be just width, both need to be set … table styling */ min-width: 176px; + max-width: 176px; } /* Multiselect bar */ @@ -140,7 +147,7 @@ table.multiselect thead th { } table.multiselect #headerName { position: relative; - width: 100%; + width: 9999px; /* when we use 100%, the styling breaks on mobile … table styling */ } table td.selection, table th.selection, table td.fileaction { width:32px; text-align:center; } table td.filename a.name { @@ -169,6 +176,15 @@ table td.filename .nametext, .uploadtext, .modified { float:left; padding:14px 0 } .modified { position: relative; + padding-left: 8px; + overflow: hidden; + text-overflow: ellipsis; + width: 90%; +} +/* ellipsize long modified dates to make room for showing delete button */ +#fileList tr:hover .modified, +#fileList tr:focus .modified { + width: 75%; } /* TODO fix usability bug (accidental file/folder selection) */ @@ -242,7 +258,7 @@ table td.filename form { font-size:14px; margin-left:48px; margin-right:48px; } #fileList tr td.filename a.name label { position: absolute; - width: 100%; + width: 80%; height: 50px; } @@ -253,6 +269,7 @@ table td.filename form { font-size:14px; margin-left:48px; margin-right:48px; } position: absolute; top: 14px; right: 0; + font-size: 11px; } #fileList img.move2trash { display:inline; margin:-8px 0; padding:16px 8px 16px 8px !important; float:right; } @@ -261,6 +278,7 @@ table td.filename form { font-size:14px; margin-left:48px; margin-right:48px; } right: 0; padding: 28px 14px 19px !important; } + a.action>img { max-height:16px; max-width:16px; vertical-align:text-bottom; } /* Actions for selected files */ @@ -290,6 +308,10 @@ a.action>img { max-height:16px; max-width:16px; vertical-align:text-bottom; } opacity: 0; display:none; } + +#fileList a.action[data-action="Rename"] { + padding:18px 14px !important; +} #fileList tr:hover a.action, #fileList a.action.permanent { -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; filter: alpha(opacity=50); diff --git a/apps/files/index.php b/apps/files/index.php index 8f6838aa0d9..dd63f29bc28 100644 --- a/apps/files/index.php +++ b/apps/files/index.php @@ -63,7 +63,6 @@ $files = array(); $user = OC_User::getUser(); if (\OC\Files\Cache\Upgrade::needUpgrade($user)) { //dont load anything if we need to upgrade the cache $needUpgrade = true; - $freeSpace = 0; } else { if ($isIE8){ // after the redirect above, the URL will have a format @@ -77,7 +76,6 @@ if (\OC\Files\Cache\Upgrade::needUpgrade($user)) { //dont load anything if we ne else{ $files = \OCA\Files\Helper::getFiles($dir); } - $freeSpace = \OC\Files\Filesystem::free_space($dir); $needUpgrade = false; } @@ -103,6 +101,8 @@ if ($needUpgrade) { } else { // information about storage capacities $storageInfo=OC_Helper::getStorageInfo($dir); + $freeSpace=$storageInfo['free']; + $uploadLimit=OCP\Util::uploadLimit(); $maxUploadFilesize=OCP\Util::maxUploadFilesize($dir); $publicUploadEnabled = \OC_Appconfig::getValue('core', 'shareapi_allow_public_upload', 'yes'); // if the encryption app is disabled, than everything is fine (INIT_SUCCESSFUL status code) @@ -134,8 +134,10 @@ if ($needUpgrade) { $tmpl->assign('files', $files); $tmpl->assign('trash', $trashEnabled); $tmpl->assign('trashEmpty', $trashEmpty); - $tmpl->assign('uploadMaxFilesize', $maxUploadFilesize); + $tmpl->assign('uploadMaxFilesize', $maxUploadFilesize); // minimium of freeSpace and uploadLimit $tmpl->assign('uploadMaxHumanFilesize', OCP\Util::humanFileSize($maxUploadFilesize)); + $tmpl->assign('freeSpace', $freeSpace); + $tmpl->assign('uploadLimit', $uploadLimit); // PHP upload limit $tmpl->assign('allowZipDownload', intval(OCP\Config::getSystemValue('allowZipDownload', true))); $tmpl->assign('usedSpacePercent', (int)$storageInfo['relative']); $tmpl->assign('isPublic', false); diff --git a/apps/files/js/admin.js b/apps/files/js/admin.js index bfa96670635..f735079fcbe 100644 --- a/apps/files/js/admin.js +++ b/apps/files/js/admin.js @@ -1,3 +1,13 @@ +/* + * Copyright (c) 2014 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + function switchPublicFolder() { var publicEnable = $('#publicEnable').is(':checked'); @@ -10,7 +20,7 @@ function switchPublicFolder() $(document).ready(function(){ switchPublicFolder(); // Execute the function after loading DOM tree $('#publicEnable').click(function(){ - switchPublicFolder(); // To get rid of onClick() + switchPublicFolder(); // To get rid of onClick() }); $('#allowZipDownload').bind('change', function() { diff --git a/apps/files/js/file-upload.js b/apps/files/js/file-upload.js index 225c3319107..f962a7044a8 100644 --- a/apps/files/js/file-upload.js +++ b/apps/files/js/file-upload.js @@ -1,3 +1,13 @@ +/* + * Copyright (c) 2014 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + /** * The file upload code uses several hooks to interact with blueimps jQuery file upload library: * 1. the core upload handling hooks are added when initializing the plugin, @@ -8,6 +18,8 @@ * - TODO music upload button */ +/* global OC, t, n */ + /** * Function that will allow us to know if Ajax uploads are supported * @link https://github.com/New-Bamboo/example-ajax-upload/blob/master/public/index.html @@ -241,10 +253,22 @@ $(document).ready(function() { // add size selection.totalBytes += file.size; - //check max upload size - if (selection.totalBytes > $('#max_upload').val()) { + // check PHP upload limit + if (selection.totalBytes > $('#upload_limit').val()) { + data.textStatus = 'sizeexceedlimit'; + data.errorThrown = t('files', 'Total file size {size1} exceeds upload limit {size2}', { + 'size1': humanFileSize(selection.totalBytes), + 'size2': humanFileSize($('#upload_limit').val()) + }); + } + + // check free space + if (selection.totalBytes > $('#free_space').val()) { data.textStatus = 'notenoughspace'; - data.errorThrown = t('files', 'Not enough space available'); + data.errorThrown = t('files', 'Not enough free space, you are uploading {size1} but only {size2} is left', { + 'size1': humanFileSize(selection.totalBytes), + 'size2': humanFileSize($('#free_space').val()) + }); } // end upload for whole selection on error @@ -315,6 +339,13 @@ $(document).ready(function() { } else { // HTTP connection problem OC.Notification.show(data.errorThrown); + if (data.result) { + var result = JSON.parse(data.result); + if (result && result[0] && result[0].data && result[0].data.code === 'targetnotfound') { + // abort upload of next files if any + OC.Upload.cancelUploads(); + } + } } //hide notification after 10 sec setTimeout(function() { diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js index 74bb711ef3d..9a69d7b3688 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -1,3 +1,15 @@ +/* + * Copyright (c) 2014 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +/* global OC, FileList */ +/* global trashBinApp */ var FileActions = { actions: {}, defaults: {}, @@ -45,8 +57,9 @@ var FileActions = { return filteredActions; }, getDefault: function (mime, type, permissions) { + var mimePart; if (mime) { - var mimePart = mime.substr(0, mime.indexOf('/')); + mimePart = mime.substr(0, mime.indexOf('/')); } var name = false; if (mime && FileActions.defaults[mime]) { @@ -71,13 +84,15 @@ var FileActions = { FileActions.currentFile = parent; var actions = FileActions.get(FileActions.getCurrentMimeType(), FileActions.getCurrentType(), FileActions.getCurrentPermissions()); var file = FileActions.getCurrentFile(); + var nameLinks; if (FileList.findFileEl(file).data('renaming')) { return; } // recreate fileactions - parent.children('a.name').find('.fileactions').remove(); - parent.children('a.name').append('<span class="fileactions" />'); + nameLinks = parent.children('a.name'); + nameLinks.find('.fileactions, .nametext .action').remove(); + nameLinks.append('<span class="fileactions" />'); var defaultAction = FileActions.getDefault(FileActions.getCurrentMimeType(), FileActions.getCurrentType(), FileActions.getCurrentPermissions()); var actionHandler = function (event) { @@ -97,21 +112,30 @@ var FileActions = { } if ((name === 'Download' || action !== defaultAction) && name !== 'Delete') { - var img = FileActions.icons[name]; + var img = FileActions.icons[name], + actionText = t('files', name), + actionContainer = 'a.name>span.fileactions'; + + if (name === 'Rename') { + // rename has only an icon which appears behind + // the file name + actionText = ''; + actionContainer = 'a.name span.nametext'; + } if (img.call) { img = img(file); } var html = '<a href="#" class="action" data-action="' + name + '">'; if (img) { - html += '<img class ="svg" src="' + img + '" /> '; + html += '<img class ="svg" src="' + img + '" />'; } - html += t('files', name) + '</a>'; + html += '<span> ' + actionText + '</span></a>'; var element = $(html); element.data('action', name); //alert(element); element.on('click', {a: null, elem: parent, actionFunc: actions[name]}, actionHandler); - parent.find('a.name>span.fileactions').append(element); + parent.find(actionContainer).append(element); } }; @@ -130,13 +154,14 @@ var FileActions = { parent.parent().children().last().find('.action.delete').remove(); if (actions['Delete']) { var img = FileActions.icons['Delete']; + var html; if (img.call) { img = img(file); } if (typeof trashBinApp !== 'undefined' && trashBinApp) { - var html = '<a href="#" original-title="' + t('files', 'Delete permanently') + '" class="action delete delete-icon" />'; + html = '<a href="#" original-title="' + t('files', 'Delete permanently') + '" class="action delete delete-icon" />'; } else { - var html = '<a href="#" class="action delete delete-icon" />'; + html = '<a href="#" class="action delete delete-icon" />'; } var element = $(html); element.data('action', actions['Delete']); @@ -163,17 +188,21 @@ var FileActions = { }; $(document).ready(function () { + var downloadScope; if ($('#allowZipDownload').val() == 1) { - var downloadScope = 'all'; + downloadScope = 'all'; } else { - var downloadScope = 'file'; + downloadScope = 'file'; } if (typeof disableDownloadActions == 'undefined' || !disableDownloadActions) { FileActions.register(downloadScope, 'Download', OC.PERMISSION_READ, function () { return OC.imagePath('core', 'actions/download'); }, function (filename) { - window.location = OC.filePath('files', 'ajax', 'download.php') + '?files=' + encodeURIComponent(filename) + '&dir=' + encodeURIComponent($('#dir').val()); + var url = FileList.getDownloadUrl(filename); + if (url) { + OC.redirect(url); + } }); } $('#fileList tr').each(function () { diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 66968ab54c7..f538af10362 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -1,4 +1,16 @@ -var FileList={ +/* + * Copyright (c) 2014 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +/* global OC, t, n, FileList, FileActions, Files */ +/* global procesSelection, dragOptions, SVGSupport, replaceSVG */ +window.FileList={ useUndo:true, postProcessList: function() { $('#fileList tr').each(function() { @@ -28,7 +40,8 @@ var FileList={ } FileList.updateFileSummary(); procesSelection(); - + + $(window).scrollTop(0); $fileList.trigger(jQuery.Event("updated")); }, createRow:function(type, name, iconurl, linktarget, size, lastModified, permissions) { @@ -191,6 +204,7 @@ var FileList={ return OC.linkTo('files', 'index.php')+"?dir="+ encodeURIComponent(dir).replace(/%2F/g, '/'); }, setCurrentDir: function(targetDir, changeUrl) { + var url; $('#dir').val(targetDir); if (changeUrl !== false) { if (window.history.pushState && changeUrl !== false) { @@ -394,7 +408,7 @@ var FileList={ } return true; }; - + form.submit(function(event) { event.stopPropagation(); event.preventDefault(); @@ -468,7 +482,7 @@ var FileList={ var basename = newname; if (newname.indexOf('.') > 0 && tr.data('type') !== 'dir') { basename = newname.substr(0, newname.lastIndexOf('.')); - } + } td.find('a.name span.nametext').text(basename); if (newname.indexOf('.') > 0 && tr.data('type') !== 'dir') { if ( ! td.find('a.name span.extension').exists() ) { @@ -477,6 +491,7 @@ var FileList={ td.find('a.name span.extension').text(newname.substr(newname.lastIndexOf('.'))); } form.remove(); + FileActions.display( tr.find('td.filename'), true); td.children('a.name').show(); } catch (error) { input.attr('title', error); @@ -780,6 +795,20 @@ var FileList={ $('#fileList tr.searchresult').each(function(i,e) { $(e).removeClass("searchresult"); }); + }, + + /** + * Returns the download URL of the given file + * @param filename file name of the file + * @param dir optional directory in which the file name is, defaults to the current directory + */ + getDownloadUrl: function(filename, dir) { + var params = { + files: filename, + dir: dir || FileList.getCurrentDirectory(), + download: null + }; + return OC.filePath('files', 'ajax', 'download.php') + '?' + OC.buildQueryString(params); } }; @@ -819,7 +848,7 @@ $(document).ready(function() { {name: 'requesttoken', value: oc_requesttoken} ]; }; - } + } }); file_upload_start.on('fileuploadadd', function(e, data) { @@ -858,7 +887,7 @@ $(document).ready(function() { */ file_upload_start.on('fileuploaddone', function(e, data) { OC.Upload.log('filelist handle fileuploaddone', e, data); - + var response; if (typeof data.result === 'string') { response = data.result; diff --git a/apps/files/js/files.js b/apps/files/js/files.js index 2fe4ab464c8..a535700c1b3 100644 --- a/apps/files/js/files.js +++ b/apps/files/js/files.js @@ -1,4 +1,16 @@ -Files={ +/* + * Copyright (c) 2014 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +/* global OC, t, n, FileList, FileActions */ +/* global getURLParameter, isPublic */ +var Files = { // file space size sync _updateStorageStatistics: function() { Files._updateStorageStatisticsTimeout = null; @@ -41,6 +53,7 @@ Files={ } if (response.data !== undefined && response.data.uploadMaxFilesize !== undefined) { $('#max_upload').val(response.data.uploadMaxFilesize); + $('#free_space').val(response.data.freeSpace); $('#upload.button').attr('original-title', response.data.maxHumanFilesize); $('#usedSpacePercent').val(response.data.usedSpacePercent); Files.displayStorageWarnings(); @@ -67,17 +80,25 @@ Files={ return fileName; }, - isFileNameValid:function (name) { - if (name === '.') { - throw t('files', '\'.\' is an invalid file name.'); - } else if (name.length === 0) { + /** + * Checks whether the given file name is valid. + * @param name file name to check + * @return true if the file name is valid. + * Throws a string exception with an error message if + * the file name is not valid + */ + isFileNameValid: function (name) { + var trimmedName = name.trim(); + if (trimmedName === '.' || trimmedName === '..') { + throw t('files', '"{name}" is an invalid file name.', {name: name}); + } else if (trimmedName.length === 0) { throw t('files', 'File name cannot be empty.'); } - // check for invalid characters - var invalid_characters = ['\\', '/', '<', '>', ':', '"', '|', '?', '*']; + var invalid_characters = + ['\\', '/', '<', '>', ':', '"', '|', '?', '*', '\n']; for (var i = 0; i < invalid_characters.length; i++) { - if (name.indexOf(invalid_characters[i]) !== -1) { + if (trimmedName.indexOf(invalid_characters[i]) !== -1) { throw t('files', "Invalid name, '\\', '/', '<', '>', ':', '\"', '|', '?' and '*' are not allowed."); } } @@ -654,10 +675,10 @@ function procesSelection() { var totalSize = 0; for(var i=0; i<selectedFiles.length; i++) { totalSize+=selectedFiles[i].size; - }; + } for(var i=0; i<selectedFolders.length; i++) { totalSize+=selectedFolders[i].size; - }; + } $('#headerSize').text(humanFileSize(totalSize)); var selection = ''; if (selectedFolders.length > 0) { @@ -751,7 +772,7 @@ Files.lazyLoadPreview = function(path, mime, ready, width, height, etag) { console.warn('Files.lazyLoadPreview(): missing etag argument'); } - if ( $('#public_upload').length ) { + if ( $('#isPublic').length ) { urlSpec.t = $('#dirToken').val(); previewURL = OC.Router.generate('core_ajax_public_preview', urlSpec); } else { @@ -769,10 +790,11 @@ Files.lazyLoadPreview = function(path, mime, ready, width, height, etag) { } img.src = previewURL; }); -} +}; function getUniqueName(name) { if (FileList.findFileEl(name).exists()) { + var numMatch; var parts=name.split('.'); var extension = ""; if (parts.length > 1) { @@ -806,7 +828,7 @@ function checkTrashStatus() { function onClickBreadcrumb(e) { var $el = $(e.target).closest('.crumb'), - $targetDir = $el.data('dir'); + $targetDir = $el.data('dir'), isPublic = !!$('#isPublic').val(); if ($targetDir !== undefined && !isPublic) { diff --git a/apps/files/js/upgrade.js b/apps/files/js/upgrade.js index 02d57fc9e6c..714adf824a1 100644 --- a/apps/files/js/upgrade.js +++ b/apps/files/js/upgrade.js @@ -1,3 +1,14 @@ +/* + * Copyright (c) 2014 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +/* global OC */ $(document).ready(function () { var eventSource, total, bar = $('#progressbar'); console.log('start'); diff --git a/apps/files/js/upload.js b/apps/files/js/upload.js index 9d9f61f600e..617cf4b1c1d 100644 --- a/apps/files/js/upload.js +++ b/apps/files/js/upload.js @@ -1,3 +1,14 @@ +/* + * Copyright (c) 2014 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +/* global OC */ function Upload(fileSelector) { if ($.support.xhrFileUpload) { return new XHRUpload(fileSelector.target.files); diff --git a/apps/files/lib/app.php b/apps/files/lib/app.php index e04ac173d55..fea88faa92a 100644 --- a/apps/files/lib/app.php +++ b/apps/files/lib/app.php @@ -59,6 +59,13 @@ class App { $result['data'] = array( 'message' => $this->l10n->t("Invalid folder name. Usage of 'Shared' is reserved.") ); + // rename to non-existing folder is denied + } else if (!$this->view->file_exists($dir)) { + $result['data'] = array('message' => (string)$this->l10n->t( + 'The target folder has been moved or deleted.', + array($dir)), + 'code' => 'targetnotfound' + ); // rename to existing file is denied } else if ($this->view->file_exists($dir . '/' . $newname)) { @@ -83,14 +90,17 @@ class App { else { $meta['type'] = 'file'; } + // these need to be set for determineIcon() + $meta['isPreviewAvailable'] = \OC::$server->getPreviewManager()->isMimeSupported($meta['mimetype']); + $meta['directory'] = $dir; $fileinfo = array( 'id' => $meta['fileid'], 'mime' => $meta['mimetype'], 'size' => $meta['size'], 'etag' => $meta['etag'], - 'directory' => $dir, + 'directory' => $meta['directory'], 'name' => $newname, - 'isPreviewAvailable' => \OC::$server->getPreviewManager()->isMimeSupported($meta['mimetype']), + 'isPreviewAvailable' => $meta['isPreviewAvailable'], 'icon' => \OCA\Files\Helper::determineIcon($meta) ); $result['success'] = true; diff --git a/apps/files/lib/helper.php b/apps/files/lib/helper.php index eaff28178ea..21d1f50e587 100644 --- a/apps/files/lib/helper.php +++ b/apps/files/lib/helper.php @@ -15,6 +15,7 @@ class Helper return array('uploadMaxFilesize' => $maxUploadFilesize, 'maxHumanFilesize' => $maxHumanFilesize, + 'freeSpace' => $storageInfo['free'], 'usedSpacePercent' => (int)$storageInfo['relative']); } diff --git a/apps/files/templates/index.php b/apps/files/templates/index.php index 5ed1ee0c7a0..939043b2c9f 100644 --- a/apps/files/templates/index.php +++ b/apps/files/templates/index.php @@ -1,6 +1,7 @@ <div id="controls"> <?php print_unescaped($_['breadcrumb']); ?> <div class="actions creatable <?php if (!$_['isCreatable']):?>hidden<?php endif; ?>"> + <?php if(!isset($_['dirToken'])):?> <div id="new" class="button"> <a><?php p($l->t('New'));?></a> <ul> @@ -12,11 +13,17 @@ data-type='web'><p><?php p($l->t('From link'));?></p></li> </ul> </div> + <?php endif;?> <div id="upload" class="button" title="<?php p($l->t('Upload') . ' max. '.$_['uploadMaxHumanFilesize']) ?>"> <?php if($_['uploadMaxFilesize'] >= 0):?> - <input type="hidden" name="MAX_FILE_SIZE" id="max_upload" - value="<?php p($_['uploadMaxFilesize']) ?>"> + <input type="hidden" id="max_upload" name="MAX_FILE_SIZE" value="<?php p($_['uploadMaxFilesize']) ?>"> + <?php endif;?> + <input type="hidden" id="upload_limit" value="<?php p($_['uploadLimit']) ?>"> + <input type="hidden" id="free_space" value="<?php p($_['freeSpace']) ?>"> + <?php if(isset($_['dirToken'])):?> + <input type="hidden" id="publicUploadRequestToken" name="requesttoken" value="<?php p($_['requesttoken']) ?>" /> + <input type="hidden" id="dirToken" name="dirToken" value="<?php p($_['dirToken']) ?>" /> <?php endif;?> <input type="hidden" class="max_human_file_size" value="(max <?php p($_['uploadMaxHumanFilesize']); ?>)"> @@ -26,7 +33,7 @@ <a href="#" class="svg icon icon-upload"></a> </div> <?php if ($_['trash']): ?> - <input id="trash" type="button" value="<?php p($l->t('Deleted files'));?>" class="button" <?php $_['trashEmpty'] ? p('disabled') : '' ?>></input> + <input id="trash" type="button" value="<?php p($l->t('Deleted files'));?>" class="button" <?php $_['trashEmpty'] ? p('disabled') : '' ?> /> <?php endif; ?> <div id="uploadprogresswrapper"> <div id="uploadprogressbar"></div> @@ -44,7 +51,7 @@ <div id="emptycontent" <?php if (!$_['emptyContent']):?>class="hidden"<?php endif; ?>><?php p($l->t('Nothing in here. Upload something!'))?></div> -<input type="hidden" id="disableSharing" data-status="<?php p($_['disableSharing']); ?>"></input> +<input type="hidden" id="disableSharing" data-status="<?php p($_['disableSharing']); ?>" /> <table id="filestable" data-allow-public-upload="<?php p($_['publicUploadEnabled'])?>" data-preview-x="36" data-preview-y="36"> <thead> diff --git a/apps/files/tests/ajax_rename.php b/apps/files/tests/ajax_rename.php index 350ff5d3687..a1a5c8983ba 100644 --- a/apps/files/tests/ajax_rename.php +++ b/apps/files/tests/ajax_rename.php @@ -38,7 +38,7 @@ class Test_OC_Files_App_Rename extends \PHPUnit_Framework_TestCase { $l10nMock->expects($this->any()) ->method('t') ->will($this->returnArgument(0)); - $viewMock = $this->getMock('\OC\Files\View', array('rename', 'normalizePath', 'getFileInfo'), array(), '', false); + $viewMock = $this->getMock('\OC\Files\View', array('rename', 'normalizePath', 'getFileInfo', 'file_exists'), array(), '', false); $viewMock->expects($this->any()) ->method('normalizePath') ->will($this->returnArgument(0)); @@ -63,6 +63,11 @@ class Test_OC_Files_App_Rename extends \PHPUnit_Framework_TestCase { $oldname = 'Shared'; $newname = 'new_name'; + $this->viewMock->expects($this->at(0)) + ->method('file_exists') + ->with('/') + ->will($this->returnValue(true)); + $result = $this->files->rename($dir, $oldname, $newname); $expected = array( 'success' => false, @@ -80,6 +85,11 @@ class Test_OC_Files_App_Rename extends \PHPUnit_Framework_TestCase { $oldname = 'Shared'; $newname = 'new_name'; + $this->viewMock->expects($this->at(0)) + ->method('file_exists') + ->with('/test') + ->will($this->returnValue(true)); + $this->viewMock->expects($this->any()) ->method('getFileInfo') ->will($this->returnValue(array( @@ -129,6 +139,11 @@ class Test_OC_Files_App_Rename extends \PHPUnit_Framework_TestCase { $oldname = 'oldname'; $newname = 'newname'; + $this->viewMock->expects($this->at(0)) + ->method('file_exists') + ->with('/') + ->will($this->returnValue(true)); + $this->viewMock->expects($this->any()) ->method('getFileInfo') ->will($this->returnValue(array( @@ -141,7 +156,6 @@ class Test_OC_Files_App_Rename extends \PHPUnit_Framework_TestCase { 'name' => 'new_name', ))); - $result = $this->files->rename($dir, $oldname, $newname); $this->assertTrue($result['success']); @@ -154,4 +168,35 @@ class Test_OC_Files_App_Rename extends \PHPUnit_Framework_TestCase { $this->assertEquals(\OC_Helper::mimetypeIcon('dir'), $result['data']['icon']); $this->assertFalse($result['data']['isPreviewAvailable']); } + + /** + * Test rename inside a folder that doesn't exist any more + */ + function testRenameInNonExistingFolder() { + $dir = '/unexist'; + $oldname = 'oldname'; + $newname = 'newname'; + + $this->viewMock->expects($this->at(0)) + ->method('file_exists') + ->with('/unexist') + ->will($this->returnValue(false)); + + $this->viewMock->expects($this->any()) + ->method('getFileInfo') + ->will($this->returnValue(array( + 'fileid' => 123, + 'type' => 'dir', + 'mimetype' => 'httpd/unix-directory', + 'size' => 18, + 'etag' => 'abcdef', + 'directory' => '/unexist', + 'name' => 'new_name', + ))); + + $result = $this->files->rename($dir, $oldname, $newname); + + $this->assertFalse($result['success']); + $this->assertEquals('targetnotfound', $result['data']['code']); + } } diff --git a/apps/files/tests/js/fileactionsSpec.js b/apps/files/tests/js/fileactionsSpec.js new file mode 100644 index 00000000000..8bbc1d3d141 --- /dev/null +++ b/apps/files/tests/js/fileactionsSpec.js @@ -0,0 +1,75 @@ +/** +* ownCloud +* +* @author Vincent Petry +* @copyright 2014 Vincent Petry <pvince81@owncloud.com> +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +* License as published by the Free Software Foundation; either +* version 3 of the License, or any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU AFFERO GENERAL PUBLIC LICENSE for more details. +* +* You should have received a copy of the GNU Affero General Public +* License along with this library. If not, see <http://www.gnu.org/licenses/>. +* +*/ + +/* global OC, FileActions, FileList */ +describe('FileActions tests', function() { + var $filesTable; + beforeEach(function() { + // init horrible parameters + var $body = $('body'); + $body.append('<input type="hidden" id="dir" value="/subdir"></input>'); + $body.append('<input type="hidden" id="permissions" value="31"></input>'); + // dummy files table + $filesTable = $body.append('<table id="filestable"></table>'); + }); + afterEach(function() { + $('#dir, #permissions, #filestable').remove(); + }); + it('calling display() sets file actions', function() { + // note: download_url is actually the link target, not the actual download URL... + var $tr = FileList.addFile('testName.txt', 1234, new Date(), false, false, {download_url: 'test/download/url'}); + + // no actions before call + expect($tr.find('.action[data-action=Download]').length).toEqual(0); + expect($tr.find('.action[data-action=Rename]').length).toEqual(0); + expect($tr.find('.action.delete').length).toEqual(0); + + FileActions.display($tr.find('td.filename'), true); + + // actions defined after cal + expect($tr.find('.action[data-action=Download]').length).toEqual(1); + expect($tr.find('.nametext .action[data-action=Rename]').length).toEqual(1); + expect($tr.find('.action.delete').length).toEqual(1); + }); + it('calling display() twice correctly replaces file actions', function() { + var $tr = FileList.addFile('testName.txt', 1234, new Date(), false, false, {download_url: 'test/download/url'}); + + FileActions.display($tr.find('td.filename'), true); + FileActions.display($tr.find('td.filename'), true); + + // actions defined after cal + expect($tr.find('.action[data-action=Download]').length).toEqual(1); + expect($tr.find('.nametext .action[data-action=Rename]').length).toEqual(1); + expect($tr.find('.action.delete').length).toEqual(1); + }); + it('redirects to download URL when clicking download', function() { + var redirectStub = sinon.stub(OC, 'redirect'); + // note: download_url is actually the link target, not the actual download URL... + var $tr = FileList.addFile('test download File.txt', 1234, new Date(), false, false, {download_url: 'test/download/url'}); + FileActions.display($tr.find('td.filename'), true); + + $tr.find('.action[data-action=Download]').click(); + + expect(redirectStub.calledOnce).toEqual(true); + expect(redirectStub.getCall(0).args[0]).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?files=test%20download%20File.txt&dir=%2Fsubdir&download'); + redirectStub.restore(); + }); +}); diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index 6b28a02989e..c26e65fc4de 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -18,25 +18,32 @@ * License along with this library. If not, see <http://www.gnu.org/licenses/>. * */ + +/* global OC, FileList */ describe('FileList tests', function() { beforeEach(function() { // init horrible parameters - $('<input type="hidden" id="dir" value="/subdir"></input>').append('body'); - $('<input type="hidden" id="permissions" value="31"></input>').append('body'); + var $body = $('body'); + $body.append('<input type="hidden" id="dir" value="/subdir"></input>'); + $body.append('<input type="hidden" id="permissions" value="31"></input>'); + // dummy files table + $body.append('<table id="filestable"></table>'); }); afterEach(function() { - $('#dir, #permissions').remove(); + $('#dir, #permissions, #filestable').remove(); }); it('generates file element with correct attributes when calling addFile', function() { var lastMod = new Date(10000); + // note: download_url is actually the link target, not the actual download URL... var $tr = FileList.addFile('testName.txt', 1234, lastMod, false, false, {download_url: 'test/download/url'}); expect($tr).toBeDefined(); expect($tr[0].tagName.toLowerCase()).toEqual('tr'); + expect($tr.find('a:first').attr('href')).toEqual('test/download/url'); expect($tr.attr('data-type')).toEqual('file'); expect($tr.attr('data-file')).toEqual('testName.txt'); expect($tr.attr('data-size')).toEqual('1234'); - //expect($tr.attr('data-permissions')).toEqual('31'); + expect($tr.attr('data-permissions')).toEqual('31'); //expect($tr.attr('data-mime')).toEqual('plain/text'); }); it('generates dir element with correct attributes when calling addDir', function() { @@ -48,7 +55,11 @@ describe('FileList tests', function() { expect($tr.attr('data-type')).toEqual('dir'); expect($tr.attr('data-file')).toEqual('testFolder'); expect($tr.attr('data-size')).toEqual('1234'); - //expect($tr.attr('data-permissions')).toEqual('31'); + expect($tr.attr('data-permissions')).toEqual('31'); //expect($tr.attr('data-mime')).toEqual('httpd/unix-directory'); }); + it('returns correct download URL', function() { + expect(FileList.getDownloadUrl('some file.txt')).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?files=some%20file.txt&dir=%2Fsubdir&download'); + expect(FileList.getDownloadUrl('some file.txt', '/anotherpath/abc')).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?files=some%20file.txt&dir=%2Fanotherpath%2Fabc&download'); + }); }); diff --git a/apps/files/tests/js/filesSpec.js b/apps/files/tests/js/filesSpec.js index 9d0a2e4f9d7..018c8ef0f3c 100644 --- a/apps/files/tests/js/filesSpec.js +++ b/apps/files/tests/js/filesSpec.js @@ -18,6 +18,8 @@ * License along with this library. If not, see <http://www.gnu.org/licenses/>. * */ + +/* global Files */ describe('Files tests', function() { describe('File name validation', function() { it('Validates correct file names', function() { @@ -36,12 +38,14 @@ describe('Files tests', function() { 'und Ümläüte sind auch willkommen' ]; for ( var i = 0; i < fileNames.length; i++ ) { + var error = false; try { expect(Files.isFileNameValid(fileNames[i])).toEqual(true); } catch (e) { - fail(); + error = e; } + expect(error).toEqual(false); } }); it('Detects invalid file names', function() { @@ -69,7 +73,7 @@ describe('Files tests', function() { var threwException = false; try { Files.isFileNameValid(fileNames[i]); - fail(); + console.error('Invalid file name not detected:', fileNames[i]); } catch (e) { threwException = true; diff --git a/apps/files_encryption/hooks/hooks.php b/apps/files_encryption/hooks/hooks.php index 09d5687e226..4c4b3f2040f 100644 --- a/apps/files_encryption/hooks/hooks.php +++ b/apps/files_encryption/hooks/hooks.php @@ -32,6 +32,8 @@ class Hooks { // file for which we want to rename the keys after the rename operation was successful
private static $renamedFiles = array();
+ // file for which we want to delete the keys after the delete operation was successful
+ private static $deleteFiles = array();
/**
* @brief Startup encryption backend upon user login
@@ -630,4 +632,66 @@ class Hooks { }
}
+ /**
+ * @brief if the file was really deleted we remove the encryption keys
+ * @param array $params
+ * @return boolean
+ */
+ public static function postDelete($params) {
+
+ if (!isset(self::$deleteFiles[$params[\OC\Files\Filesystem::signal_param_path]])) {
+ return true;
+ }
+
+ $deletedFile = self::$deleteFiles[$params[\OC\Files\Filesystem::signal_param_path]];
+ $path = $deletedFile['path'];
+ $user = $deletedFile['uid'];
+
+ // we don't need to remember the file any longer
+ unset(self::$deleteFiles[$params[\OC\Files\Filesystem::signal_param_path]]);
+
+ $view = new \OC\Files\View('/');
+
+ // return if the file still exists and wasn't deleted correctly
+ if ($view->file_exists('/' . $user . '/files/' . $path)) {
+ return true;
+ }
+
+ // Disable encryption proxy to prevent recursive calls
+ $proxyStatus = \OC_FileProxy::$enabled;
+ \OC_FileProxy::$enabled = false;
+
+ // Delete keyfile & shareKey so it isn't orphaned
+ if (!Keymanager::deleteFileKey($view, $path, $user)) {
+ \OCP\Util::writeLog('Encryption library',
+ 'Keyfile or shareKey could not be deleted for file "' . $user.'/files/'.$path . '"', \OCP\Util::ERROR);
+ }
+
+ Keymanager::delAllShareKeys($view, $user, $path);
+
+ \OC_FileProxy::$enabled = $proxyStatus;
+ }
+
+ /**
+ * @brief remember the file which should be deleted and it's owner
+ * @param array $params
+ * @return boolean
+ */
+ public static function preDelete($params) {
+ $path = $params[\OC\Files\Filesystem::signal_param_path];
+
+ // skip this method if the trash bin is enabled or if we delete a file
+ // outside of /data/user/files
+ if (\OCP\App::isEnabled('files_trashbin')) {
+ return true;
+ }
+
+ $util = new Util(new \OC_FilesystemView('/'), \OCP\USER::getUser());
+ list($owner, $ownerPath) = $util->getUidAndFilename($path);
+
+ self::$deleteFiles[$params[\OC\Files\Filesystem::signal_param_path]] = array(
+ 'uid' => $owner,
+ 'path' => $ownerPath);
+ }
+
}
diff --git a/apps/files_encryption/lib/helper.php b/apps/files_encryption/lib/helper.php index 5dcb05fa196..bb06a57c714 100755 --- a/apps/files_encryption/lib/helper.php +++ b/apps/files_encryption/lib/helper.php @@ -63,6 +63,8 @@ class Helper { \OCP\Util::connectHook('OC_Filesystem', 'rename', 'OCA\Encryption\Hooks', 'preRename'); \OCP\Util::connectHook('OC_Filesystem', 'post_rename', 'OCA\Encryption\Hooks', 'postRename'); + \OCP\Util::connectHook('OC_Filesystem', 'post_delete', 'OCA\Encryption\Hooks', 'postDelete'); + \OCP\Util::connectHook('OC_Filesystem', 'delete', 'OCA\Encryption\Hooks', 'preDelete'); } /** diff --git a/apps/files_encryption/lib/keymanager.php b/apps/files_encryption/lib/keymanager.php index b2c756894b4..7abc565f609 100755 --- a/apps/files_encryption/lib/keymanager.php +++ b/apps/files_encryption/lib/keymanager.php @@ -214,15 +214,24 @@ class Keymanager { * * @param \OC_FilesystemView $view * @param string $path path of the file the key belongs to + * @param string $userId the user to whom the file belongs * @return bool Outcome of unlink operation * @note $path must be relative to data/user/files. e.g. mydoc.txt NOT * /data/admin/files/mydoc.txt */ - public static function deleteFileKey(\OC_FilesystemView $view, $path) { + public static function deleteFileKey($view, $path, $userId=null) { $trimmed = ltrim($path, '/'); - $userId = Helper::getUser($path); + if ($trimmed === '') { + \OCP\Util::writeLog('Encryption library', + 'Can\'t delete file-key empty path given!', \OCP\Util::ERROR); + return false; + } + + if ($userId === null) { + $userId = Helper::getUser($path); + } $util = new Util($view, $userId); if($util->isSystemWideMountPoint($path)) { @@ -402,7 +411,15 @@ class Keymanager { * @param string $userId owner of the file * @param string $filePath path to the file, relative to the owners file dir */ - public static function delAllShareKeys(\OC_FilesystemView $view, $userId, $filePath) { + public static function delAllShareKeys($view, $userId, $filePath) { + + $filePath = ltrim($filePath, '/'); + + if ($filePath === '') { + \OCP\Util::writeLog('Encryption library', + 'Can\'t delete share-keys empty path given!', \OCP\Util::ERROR); + return false; + } $util = new util($view, $userId); @@ -413,17 +430,15 @@ class Keymanager { } - if ($view->is_dir($userId . '/files/' . $filePath)) { + if ($view->is_dir($baseDir . $filePath)) { $view->unlink($baseDir . $filePath); } else { - $localKeyPath = $view->getLocalFile($baseDir . $filePath); - $escapedPath = Helper::escapeGlobPattern($localKeyPath); - $matches = glob($escapedPath . '*.shareKey'); - foreach ($matches as $ma) { - $result = unlink($ma); - if (!$result) { - \OCP\Util::writeLog('Encryption library', - 'Keyfile or shareKey could not be deleted for file "' . $filePath . '"', \OCP\Util::ERROR); + $parentDir = dirname($baseDir . $filePath); + $filename = pathinfo($filePath, PATHINFO_BASENAME); + foreach($view->getDirectoryContent($parentDir) as $content) { + $path = $content['path']; + if (self::getFilenameFromShareKey($content['name']) === $filename) { + $view->unlink('/' . $userId . '/' . $path); } } } @@ -523,4 +538,20 @@ class Keymanager { return $targetPath; } + + /** + * @brief extract filename from share key name + * @param string $shareKey (filename.userid.sharekey) + * @return mixed filename or false + */ + protected static function getFilenameFromShareKey($shareKey) { + $parts = explode('.', $shareKey); + + $filename = false; + if(count($parts) > 2) { + $filename = implode('.', array_slice($parts, 0, count($parts)-2)); + } + + return $filename; + } } diff --git a/apps/files_encryption/lib/proxy.php b/apps/files_encryption/lib/proxy.php index 4e71ab1dd5d..11048005969 100644 --- a/apps/files_encryption/lib/proxy.php +++ b/apps/files_encryption/lib/proxy.php @@ -204,47 +204,6 @@ class Proxy extends \OC_FileProxy { } /** - * @brief When a file is deleted, remove its keyfile also - */ - public function preUnlink($path) { - - $relPath = Helper::stripUserFilesPath($path); - - // skip this method if the trash bin is enabled or if we delete a file - // outside of /data/user/files - if (\OCP\App::isEnabled('files_trashbin') || $relPath === false) { - return true; - } - - // Disable encryption proxy to prevent recursive calls - $proxyStatus = \OC_FileProxy::$enabled; - \OC_FileProxy::$enabled = false; - - $view = new \OC_FilesystemView('/'); - - $userId = \OCP\USER::getUser(); - - $util = new Util($view, $userId); - - list($owner, $ownerPath) = $util->getUidAndFilename($relPath); - - // Delete keyfile & shareKey so it isn't orphaned - if (!Keymanager::deleteFileKey($view, $ownerPath)) { - \OCP\Util::writeLog('Encryption library', - 'Keyfile or shareKey could not be deleted for file "' . $ownerPath . '"', \OCP\Util::ERROR); - } - - Keymanager::delAllShareKeys($view, $owner, $ownerPath); - - \OC_FileProxy::$enabled = $proxyStatus; - - // If we don't return true then file delete will fail; better - // to leave orphaned keyfiles than to disallow file deletion - return true; - - } - - /** * @param $path * @return bool */ diff --git a/apps/files_encryption/lib/util.php b/apps/files_encryption/lib/util.php index 8816d4d649a..ae3e2a2e15a 100644 --- a/apps/files_encryption/lib/util.php +++ b/apps/files_encryption/lib/util.php @@ -57,7 +57,7 @@ class Util { * @param $userId * @param bool $client */ - public function __construct(\OC_FilesystemView $view, $userId, $client = false) { + public function __construct($view, $userId, $client = false) { $this->view = $view; $this->client = $client; diff --git a/apps/files_encryption/tests/hooks.php b/apps/files_encryption/tests/hooks.php new file mode 100644 index 00000000000..44525791743 --- /dev/null +++ b/apps/files_encryption/tests/hooks.php @@ -0,0 +1,271 @@ +<?php +/** + * ownCloud + * + * @author Bjoern Schiessle + * @copyright 2014 Bjoern Schiessle <schiessle@owncloud.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + */ + +require_once __DIR__ . '/../../../lib/base.php'; +require_once __DIR__ . '/../lib/crypt.php'; +require_once __DIR__ . '/../lib/keymanager.php'; +require_once __DIR__ . '/../lib/stream.php'; +require_once __DIR__ . '/../lib/util.php'; +require_once __DIR__ . '/../appinfo/app.php'; +require_once __DIR__ . '/util.php'; + +use OCA\Encryption; + +/** + * Class Test_Encryption_Hooks + * @brief this class provide basic hook app tests + */ +class Test_Encryption_Hooks extends \PHPUnit_Framework_TestCase { + + const TEST_ENCRYPTION_HOOKS_USER1 = "test-encryption-hooks-user1"; + const TEST_ENCRYPTION_HOOKS_USER2 = "test-encryption-hooks-user2"; + + /** + * @var \OC_FilesystemView + */ + public $user1View; // view on /data/user1/files + public $user2View; // view on /data/user2/files + public $rootView; // view on /data/user + public $data; + public $filename; + + public static function setUpBeforeClass() { + // reset backend + \OC_User::clearBackends(); + \OC_User::useBackend('database'); + + \OC_Hook::clear('OC_Filesystem'); + \OC_Hook::clear('OC_User'); + + // clear share hooks + \OC_Hook::clear('OCP\\Share'); + \OC::registerShareHooks(); + \OCP\Util::connectHook('OC_Filesystem', 'setup', '\OC\Files\Storage\Shared', 'setup'); + + // Filesystem related hooks + \OCA\Encryption\Helper::registerFilesystemHooks(); + + // Sharing related hooks + \OCA\Encryption\Helper::registerShareHooks(); + + // clear and register proxies + \OC_FileProxy::clearProxies(); + \OC_FileProxy::register(new OCA\Encryption\Proxy()); + + // create test user + \Test_Encryption_Util::loginHelper(\Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER1, true); + \Test_Encryption_Util::loginHelper(\Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER2, true); + } + + function setUp() { + // set user id + \Test_Encryption_Util::loginHelper(\Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER1); + \OC_User::setUserId(\Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER1); + + // init filesystem view + $this->user1View = new \OC_FilesystemView('/'. \Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER1 . '/files'); + $this->user2View = new \OC_FilesystemView('/'. \Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER2 . '/files'); + $this->rootView = new \OC_FilesystemView('/'); + + // init short data + $this->data = 'hats'; + $this->filename = 'enc_hooks_tests-' . uniqid() . '.txt'; + + } + + public static function tearDownAfterClass() { + // cleanup test user + \OC_User::deleteUser(\Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER1); + \OC_User::deleteUser(\Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER2); + } + + function testDeleteHooks() { + + // remember files_trashbin state + $stateFilesTrashbin = OC_App::isEnabled('files_trashbin'); + + // we want to tests with app files_trashbin disabled + \OC_App::disable('files_trashbin'); + + // make sure that the trash bin is disabled + $this->assertFalse(\OC_APP::isEnabled('files_trashbin')); + + $this->user1View->file_put_contents($this->filename, $this->data); + + // check if all keys are generated + $this->assertTrue($this->rootView->file_exists( + self::TEST_ENCRYPTION_HOOKS_USER1 . '/files_encryption/share-keys/' + . $this->filename . '.' . \Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER1 . '.shareKey')); + $this->assertTrue($this->rootView->file_exists( + self::TEST_ENCRYPTION_HOOKS_USER1 . '/files_encryption/keyfiles/' . $this->filename . '.key')); + + + \Test_Encryption_Util::logoutHelper(); + \Test_Encryption_Util::loginHelper(\Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER2); + \OC_User::setUserId(\Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER2); + + + $this->user2View->file_put_contents($this->filename, $this->data); + + // check if all keys are generated + $this->assertTrue($this->rootView->file_exists( + self::TEST_ENCRYPTION_HOOKS_USER2 . '/files_encryption/share-keys/' + . $this->filename . '.' . \Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER2 . '.shareKey')); + $this->assertTrue($this->rootView->file_exists( + self::TEST_ENCRYPTION_HOOKS_USER2 . '/files_encryption/keyfiles/' . $this->filename . '.key')); + + + // create a dummy file that we can delete something outside of data/user/files + // in this case no share or file keys should be deleted + $this->rootView->file_put_contents(self::TEST_ENCRYPTION_HOOKS_USER2 . "/" . $this->filename, $this->data); + + // delete dummy file outside of data/user/files + $this->rootView->unlink(self::TEST_ENCRYPTION_HOOKS_USER2 . "/" . $this->filename); + + // all keys should still exist + $this->assertTrue($this->rootView->file_exists( + self::TEST_ENCRYPTION_HOOKS_USER2 . '/files_encryption/share-keys/' + . $this->filename . '.' . \Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER2 . '.shareKey')); + $this->assertTrue($this->rootView->file_exists( + self::TEST_ENCRYPTION_HOOKS_USER2 . '/files_encryption/keyfiles/' . $this->filename . '.key')); + + + // delete the file in data/user/files + // now the correspondig share and file keys from user2 should be deleted + $this->user2View->unlink($this->filename); + + // check if keys from user2 are really deleted + $this->assertFalse($this->rootView->file_exists( + self::TEST_ENCRYPTION_HOOKS_USER2 . '/files_encryption/share-keys/' + . $this->filename . '.' . \Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER2 . '.shareKey')); + $this->assertFalse($this->rootView->file_exists( + self::TEST_ENCRYPTION_HOOKS_USER2 . '/files_encryption/keyfiles/' . $this->filename . '.key')); + + // but user1 keys should still exist + $this->assertTrue($this->rootView->file_exists( + self::TEST_ENCRYPTION_HOOKS_USER1 . '/files_encryption/share-keys/' + . $this->filename . '.' . \Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER1 . '.shareKey')); + $this->assertTrue($this->rootView->file_exists( + self::TEST_ENCRYPTION_HOOKS_USER1 . '/files_encryption/keyfiles/' . $this->filename . '.key')); + + if ($stateFilesTrashbin) { + OC_App::enable('files_trashbin'); + } + else { + OC_App::disable('files_trashbin'); + } + } + + function testDeleteHooksForSharedFiles() { + + \Test_Encryption_Util::logoutHelper(); + \Test_Encryption_Util::loginHelper(\Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER1); + \OC_User::setUserId(\Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER1); + + // remember files_trashbin state + $stateFilesTrashbin = OC_App::isEnabled('files_trashbin'); + + // we want to tests with app files_trashbin disabled + \OC_App::disable('files_trashbin'); + + // make sure that the trash bin is disabled + $this->assertFalse(\OC_APP::isEnabled('files_trashbin')); + + $this->user1View->file_put_contents($this->filename, $this->data); + + // check if all keys are generated + $this->assertTrue($this->rootView->file_exists( + self::TEST_ENCRYPTION_HOOKS_USER1 . '/files_encryption/share-keys/' + . $this->filename . '.' . \Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER1 . '.shareKey')); + $this->assertTrue($this->rootView->file_exists( + self::TEST_ENCRYPTION_HOOKS_USER1 . '/files_encryption/keyfiles/' . $this->filename . '.key')); + + // get the file info from previous created file + $fileInfo = $this->user1View->getFileInfo($this->filename); + + // check if we have a valid file info + $this->assertTrue(is_array($fileInfo)); + + // share the file with user2 + \OCP\Share::shareItem('file', $fileInfo['fileid'], \OCP\Share::SHARE_TYPE_USER, self::TEST_ENCRYPTION_HOOKS_USER2, OCP\PERMISSION_ALL); + + // check if new share key exists + $this->assertTrue($this->rootView->file_exists( + self::TEST_ENCRYPTION_HOOKS_USER1 . '/files_encryption/share-keys/' + . $this->filename . '.' . \Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER2 . '.shareKey')); + + \Test_Encryption_Util::logoutHelper(); + \Test_Encryption_Util::loginHelper(\Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER2); + \OC_User::setUserId(\Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER2); + + // user2 has a local file with the same name + $this->user2View->file_put_contents($this->filename, $this->data); + + // check if all keys are generated + $this->assertTrue($this->rootView->file_exists( + self::TEST_ENCRYPTION_HOOKS_USER2 . '/files_encryption/share-keys/' + . $this->filename . '.' . \Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER2 . '.shareKey')); + $this->assertTrue($this->rootView->file_exists( + self::TEST_ENCRYPTION_HOOKS_USER2 . '/files_encryption/keyfiles/' . $this->filename . '.key')); + + // delete the Shared file from user1 in data/user2/files/Shared + $this->user2View->unlink('/Shared/' . $this->filename); + + // now keys from user1s home should be gone + $this->assertFalse($this->rootView->file_exists( + self::TEST_ENCRYPTION_HOOKS_USER1 . '/files_encryption/share-keys/' + . $this->filename . '.' . \Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER1 . '.shareKey')); + $this->assertFalse($this->rootView->file_exists( + self::TEST_ENCRYPTION_HOOKS_USER1 . '/files_encryption/share-keys/' + . $this->filename . '.' . \Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER2 . '.shareKey')); + $this->assertFalse($this->rootView->file_exists( + self::TEST_ENCRYPTION_HOOKS_USER1 . '/files_encryption/keyfiles/' . $this->filename . '.key')); + + // but user2 keys should still exist + $this->assertTrue($this->rootView->file_exists( + self::TEST_ENCRYPTION_HOOKS_USER2 . '/files_encryption/share-keys/' + . $this->filename . '.' . \Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER2 . '.shareKey')); + $this->assertTrue($this->rootView->file_exists( + self::TEST_ENCRYPTION_HOOKS_USER2 . '/files_encryption/keyfiles/' . $this->filename . '.key')); + + // cleanup + + $this->user2View->unlink($this->filename); + + \Test_Encryption_Util::logoutHelper(); + \Test_Encryption_Util::loginHelper(\Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER1); + \OC_User::setUserId(\Test_Encryption_Hooks::TEST_ENCRYPTION_HOOKS_USER1); + + // unshare the file + \OCP\Share::unshare('file', $fileInfo['fileid'], \OCP\Share::SHARE_TYPE_USER, self::TEST_ENCRYPTION_HOOKS_USER2); + + $this->user1View->unlink($this->filename); + + if ($stateFilesTrashbin) { + OC_App::enable('files_trashbin'); + } + else { + OC_App::disable('files_trashbin'); + } + } + +} diff --git a/apps/files_encryption/tests/keymanager.php b/apps/files_encryption/tests/keymanager.php index 58a57ee5af4..6f32c50743c 100644 --- a/apps/files_encryption/tests/keymanager.php +++ b/apps/files_encryption/tests/keymanager.php @@ -137,6 +137,17 @@ class Test_Encryption_Keymanager extends \PHPUnit_Framework_TestCase { } /** + * @small + */ + function testGetFilenameFromShareKey() { + $this->assertEquals("file", + \TestProtectedKeymanagerMethods::testGetFilenameFromShareKey("file.user.shareKey")); + $this->assertEquals("file.name.with.dots", + \TestProtectedKeymanagerMethods::testGetFilenameFromShareKey("file.name.with.dots.user.shareKey")); + $this->assertFalse(\TestProtectedKeymanagerMethods::testGetFilenameFromShareKey("file.txt")); + } + + /** * @medium */ function testSetFileKey() { @@ -234,3 +245,12 @@ class Test_Encryption_Keymanager extends \PHPUnit_Framework_TestCase { \OC_FileProxy::$enabled = $proxyStatus; } } + +/** + * dummy class to access protected methods of \OCA\Encryption\Keymanager for testing + */ +class TestProtectedKeymanagerMethods extends \OCA\Encryption\Keymanager { + public static function testGetFilenameFromShareKey($sharekey) { + return self::getFilenameFromShareKey($sharekey); + } +}
\ No newline at end of file diff --git a/apps/files_encryption/tests/proxy.php b/apps/files_encryption/tests/proxy.php index c3006274d6d..51cc0b795e3 100644 --- a/apps/files_encryption/tests/proxy.php +++ b/apps/files_encryption/tests/proxy.php @@ -112,54 +112,4 @@ class Test_Encryption_Proxy extends \PHPUnit_Framework_TestCase { } - function testPreUnlinkWithoutTrash() { - - // remember files_trashbin state - $stateFilesTrashbin = OC_App::isEnabled('files_trashbin'); - - // we want to tests with app files_trashbin enabled - \OC_App::disable('files_trashbin'); - - $this->view->file_put_contents($this->filename, $this->data); - - // create a dummy file that we can delete something outside of data/user/files - $this->rootView->file_put_contents("dummy.txt", $this->data); - - // check if all keys are generated - $this->assertTrue($this->rootView->file_exists( - '/files_encryption/share-keys/' - . $this->filename . '.' . \Test_Encryption_Proxy::TEST_ENCRYPTION_PROXY_USER1 . '.shareKey')); - $this->assertTrue($this->rootView->file_exists( - '/files_encryption/keyfiles/' . $this->filename . '.key')); - - - // delete dummy file outside of data/user/files - $this->rootView->unlink("dummy.txt"); - - // all keys should still exist - $this->assertTrue($this->rootView->file_exists( - '/files_encryption/share-keys/' - . $this->filename . '.' . \Test_Encryption_Proxy::TEST_ENCRYPTION_PROXY_USER1 . '.shareKey')); - $this->assertTrue($this->rootView->file_exists( - '/files_encryption/keyfiles/' . $this->filename . '.key')); - - - // delete the file in data/user/files - $this->view->unlink($this->filename); - - // now also the keys should be gone - $this->assertFalse($this->rootView->file_exists( - '/files_encryption/share-keys/' - . $this->filename . '.' . \Test_Encryption_Proxy::TEST_ENCRYPTION_PROXY_USER1 . '.shareKey')); - $this->assertFalse($this->rootView->file_exists( - '/files_encryption/keyfiles/' . $this->filename . '.key')); - - if ($stateFilesTrashbin) { - OC_App::enable('files_trashbin'); - } - else { - OC_App::disable('files_trashbin'); - } - } - } diff --git a/apps/files_encryption/tests/share.php b/apps/files_encryption/tests/share.php index e55427620a6..acf408a07f0 100755 --- a/apps/files_encryption/tests/share.php +++ b/apps/files_encryption/tests/share.php @@ -194,8 +194,9 @@ class Test_Encryption_Share extends \PHPUnit_Framework_TestCase { . $this->filename . '.' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER2 . '.shareKey')); // cleanup - $this->view->unlink( - '/' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER1 . '/files/' . $this->filename); + $this->view->chroot('/' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER1 . '/files/'); + $this->view->unlink($this->filename); + $this->view->chroot('/'); // check if share key not exists $this->assertFalse($this->view->file_exists( @@ -265,8 +266,9 @@ class Test_Encryption_Share extends \PHPUnit_Framework_TestCase { . $this->filename . '.' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER2 . '.shareKey')); // cleanup - $this->view->unlink( - '/' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER1 . '/files/' . $this->filename); + $this->view->chroot('/' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER1 . '/files/'); + $this->view->unlink($this->filename); + $this->view->chroot('/'); // check if share key not exists $this->assertFalse($this->view->file_exists( @@ -352,7 +354,9 @@ class Test_Encryption_Share extends \PHPUnit_Framework_TestCase { . $this->filename . '.' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER2 . '.shareKey')); // cleanup - $this->view->unlink('/' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER1 . '/files' . $this->folder1); + $this->view->chroot('/' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER1 . '/files'); + $this->view->unlink($this->folder1); + $this->view->chroot('/'); // check if share key not exists $this->assertFalse($this->view->file_exists( @@ -482,9 +486,9 @@ class Test_Encryption_Share extends \PHPUnit_Framework_TestCase { . $this->filename . '.' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER2 . '.shareKey')); // cleanup - $this->view->unlink( - '/' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER1 . '/files' . $this->folder1 . $this->subfolder - . $this->subsubfolder . '/' . $this->filename); + $this->view->chroot('/' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER1 . '/files'); + $this->view->unlink($this->folder1 . $this->subfolder . $this->subsubfolder . '/' . $this->filename); + $this->view->chroot('/'); // check if share key not exists $this->assertFalse($this->view->file_exists( @@ -559,7 +563,9 @@ class Test_Encryption_Share extends \PHPUnit_Framework_TestCase { . $this->filename . '.' . $publicShareKeyId . '.shareKey')); // cleanup - $this->view->unlink('/' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER1 . '/files/' . $this->filename); + $this->view->chroot('/' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER1 . '/files/'); + $this->view->unlink($this->filename); + $this->view->chroot('/'); // check if share key not exists $this->assertFalse($this->view->file_exists( @@ -636,7 +642,9 @@ class Test_Encryption_Share extends \PHPUnit_Framework_TestCase { . $this->filename . '.' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER4 . '.shareKey')); // cleanup - $this->view->unlink('/' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER1 . '/files/' . $this->filename); + $this->view->chroot('/' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER1 . '/files/'); + $this->view->unlink($this->filename); + $this->view->chroot('/'); // check if share key not exists $this->assertFalse($this->view->file_exists( @@ -731,8 +739,10 @@ class Test_Encryption_Share extends \PHPUnit_Framework_TestCase { . $this->filename . '.' . $recoveryKeyId . '.shareKey')); // cleanup - $this->view->unlink('/' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER1 . '/files/' . $this->filename); - $this->view->unlink('/' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER1 . '/files/' . $this->folder1); + $this->view->chroot('/' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER1 . '/files/'); + $this->view->unlink($this->filename); + $this->view->unlink($this->folder1); + $this->view->chroot('/'); // check if share key for recovery not exists $this->assertFalse($this->view->file_exists( @@ -828,8 +838,10 @@ class Test_Encryption_Share extends \PHPUnit_Framework_TestCase { $this->assertEquals($this->dataShort, $retrievedCryptedFile2); // cleanup - $this->view->unlink('/' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER2 . '/files/' . $this->folder1); - $this->view->unlink('/' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER2 . '/files/' . $this->filename); + $this->view->chroot('/' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER2 . '/files/'); + $this->view->unlink($this->folder1); + $this->view->unlink($this->filename); + $this->view->chroot('/'); // check if share key for user and recovery exists $this->assertFalse($this->view->file_exists( @@ -930,7 +942,9 @@ class Test_Encryption_Share extends \PHPUnit_Framework_TestCase { . $this->filename . '.' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER3 . '.shareKey')); // cleanup - $this->view->unlink('/' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER1 . '/files/' . $this->filename); + $this->view->chroot('/' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER1 . '/files/'); + $this->view->unlink($this->filename); + $this->view->chroot('/'); } } diff --git a/apps/files_sharing/css/mobile.css b/apps/files_sharing/css/mobile.css new file mode 100644 index 00000000000..7d2116d190d --- /dev/null +++ b/apps/files_sharing/css/mobile.css @@ -0,0 +1,49 @@ +@media only screen and (max-width: 600px) { + +/* make header scroll up for single shares, more view of content on small screens */ +#header.share-file { + position: absolute !important; +} + +/* hide size and date columns */ +table th#headerSize, +table td.filesize, +table th#headerDate, +table td.date { + display: none; +} + +/* restrict length of displayed filename to prevent overflow */ +table td.filename .nametext { + max-width: 75% !important; +} + +/* on mobile, show single shared image at full width without margin */ +#imgframe { + width: 100%; + padding: 0; + margin-bottom: 35px; +} +/* some margin for the file type icon */ +#imgframe .publicpreview { + margin-top: 32px; +} + +/* always show actions on mobile */ +#fileList a.action { + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)" !important; + filter: alpha(opacity=20) !important; + opacity: .2 !important; + display: inline !important; +} +/* some padding for better clickability */ +#fileList a.action img { + padding: 0 6px 0 12px; +} +/* hide text of the actions on mobile */ +#fileList a.action span { + display: none; +} + + +} diff --git a/apps/files_sharing/css/public.css b/apps/files_sharing/css/public.css index 6e0c6eb75b9..21f0c82b829 100644 --- a/apps/files_sharing/css/public.css +++ b/apps/files_sharing/css/public.css @@ -14,39 +14,17 @@ body { padding:7px; } -#details { - color:#fff; - float: left; -} - -#public_upload, -#download { - font-weight:700; - margin: 0 0 0 6px; - padding: 0 5px; - height: 32px; - float: left; - -} - -.header-right #details { - margin-right: 28px; -} - .header-right { padding: 0; height: 32px; } -#public_upload { - margin-left: 5px; -} - -#public_upload img, -#download img { - padding-left:2px; - padding-right:5px; - vertical-align:text-bottom; +#details { + color:#fff; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; + filter: alpha(opacity=50); + opacity: .5; + padding-right: 5px; } #controls { @@ -71,9 +49,8 @@ footer { p.info { color: #777; text-align: center; - width: 352px; margin: 0 auto; - padding: 20px; + padding: 20px 0; } p.info a { @@ -94,9 +71,13 @@ p.info a { max-width:100%; } -thead{ - background-color: white; - padding-left:0 !important; /* fixes multiselect bar offset on shared page */ +/* some margin for the file type icon */ +#imgframe .publicpreview { + margin-top: 10%; +} + +thead { + padding-left: 0 !important; /* fixes multiselect bar offset on shared page */ } #data-upload-form { @@ -110,27 +91,20 @@ thead{ margin: 0; } -#file_upload_start { - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; - filter: alpha(opacity=0); - opacity: 0; - z-index: 20; - position: absolute !important; - top: 0; - left: 0; - width: 100% !important; -} - +.directDownload, .directLink { margin-bottom: 20px; } +.directDownload .button img { + vertical-align: text-bottom; +} .directLink label { font-weight: normal; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; + filter: alpha(opacity=50); + opacity: .5; } .directLink input { - margin-left: 10px; + margin-left: 5px; width: 300px; } -.public_actions { - padding: 4px; -} diff --git a/apps/files_sharing/js/public.js b/apps/files_sharing/js/public.js index 4c0b0ad9d48..c1b7eee3fb7 100644 --- a/apps/files_sharing/js/public.js +++ b/apps/files_sharing/js/public.js @@ -9,22 +9,13 @@ function fileDownloadPath(dir, file) { $(document).ready(function() { - $('#data-upload-form').tipsy({gravity:'ne', fade:true}); - if (typeof FileActions !== 'undefined') { var mimetype = $('#mimetype').val(); // Show file preview if previewer is available, images are already handled by the template if (mimetype.substr(0, mimetype.indexOf('/')) != 'image' && $('.publicpreview').length === 0) { // Trigger default action if not download TODO var action = FileActions.getDefault(mimetype, 'file', OC.PERMISSION_READ); - if (typeof action === 'undefined') { - $('#noPreview').show(); - if (mimetype != 'httpd/unix-directory') { - // NOTE: Remove when a better file previewer solution exists - $('#content').remove(); - $('table').remove(); - } - } else { + if (typeof action !== 'undefined') { action($('#filename').val()); } } @@ -34,18 +25,16 @@ $(document).ready(function() { window.location = $(tr).find('a.name').attr('href'); } }); - FileActions.register('file', 'Download', OC.PERMISSION_READ, '', function(filename) { - var tr = FileList.findFileEl(filename); - if (tr.length > 0) { - window.location = $(tr).find('a.name').attr('href'); - } - }); - FileActions.register('dir', 'Download', OC.PERMISSION_READ, '', function(filename) { + + // override since the format is different + FileList.getDownloadUrl = function(filename, dir) { + // we use this because we need the service and token attributes var tr = FileList.findFileEl(filename); if (tr.length > 0) { - window.location = $(tr).find('a.name').attr('href')+'&download'; + return $(tr).find('a.name').attr('href') + '&download'; } - }); + return null; + }; } var file_upload_start = $('#file_upload_start'); @@ -58,16 +47,9 @@ $(document).ready(function() { }; }); - // Add Uploadprogress Wrapper to controls bar - $('#controls').append($('#controls .actions div#uploadprogresswrapper')); - $('#uploadprogresswrapper').addClass('public_actions'); - - // Cancel upload trigger - $('#cancel_upload_button').click(function() { - OC.Upload.cancelUploads(); - procesSelection(); + $(document).on('click', '#directLink', function() { + $(this).focus(); + $(this).select(); }); - $('#directLink').focus(); - }); diff --git a/apps/files_sharing/lib/api.php b/apps/files_sharing/lib/api.php index 84e90c71681..061e60ad8ed 100644 --- a/apps/files_sharing/lib/api.php +++ b/apps/files_sharing/lib/api.php @@ -162,7 +162,7 @@ class Api { $view = new \OC\Files\View('/'.\OCP\User::getUser().'/files'); if(!$view->is_dir($path)) { - return new \OC_OCS_Result(null, 404, "not a directory"); + return new \OC_OCS_Result(null, 400, "not a directory"); } $content = $view->getDirectoryContent($path); @@ -178,8 +178,7 @@ class Api { $share['received_from_displayname'] = \OCP\User::getDisplayName($receivedFrom['uid_owner']); } if ($share) { - $share['filename'] = $file['name']; - $result[] = $share; + $result = array_merge($result, $share); } } @@ -220,10 +219,8 @@ class Api { $shareWith = isset($_POST['password']) ? $_POST['password'] : null; //check public link share $publicUploadEnabled = \OC_Appconfig::getValue('core', 'shareapi_allow_public_upload', 'yes'); - $encryptionEnabled = \OC_App::isEnabled('files_encryption'); - if(isset($_POST['publicUpload']) && - ($encryptionEnabled || $publicUploadEnabled !== 'yes')) { - return new \OC_OCS_Result(null, 404, "public upload disabled by the administrator"); + if(isset($_POST['publicUpload']) && $publicUploadEnabled !== 'yes') { + return new \OC_OCS_Result(null, 403, "public upload disabled by the administrator"); } $publicUpload = isset($_POST['publicUpload']) ? $_POST['publicUpload'] : 'false'; // read, create, update (7) if public upload is enabled or @@ -231,7 +228,7 @@ class Api { $permissions = $publicUpload === 'true' ? 7 : 1; break; default: - return new \OC_OCS_Result(null, 404, "unknown share type"); + return new \OC_OCS_Result(null, 400, "unknown share type"); } try { @@ -243,7 +240,7 @@ class Api { $permissions ); } catch (\Exception $e) { - return new \OC_OCS_Result(null, 404, $e->getMessage()); + return new \OC_OCS_Result(null, 403, $e->getMessage()); } if ($token) { @@ -321,11 +318,8 @@ class Api { $permissions = isset($params['_put']['permissions']) ? (int)$params['_put']['permissions'] : null; $publicUploadStatus = \OC_Appconfig::getValue('core', 'shareapi_allow_public_upload', 'yes'); - $encryptionEnabled = \OC_App::isEnabled('files_encryption'); - $publicUploadEnabled = false; - if(!$encryptionEnabled && $publicUploadStatus === 'yes') { - $publicUploadEnabled = true; - } + $publicUploadEnabled = ($publicUploadStatus === 'yes') ? true : false; + // only change permissions for public shares if public upload is enabled // and we want to set permissions to 1 (read only) or 7 (allow upload) @@ -363,9 +357,8 @@ class Api { private static function updatePublicUpload($share, $params) { $publicUploadEnabled = \OC_Appconfig::getValue('core', 'shareapi_allow_public_upload', 'yes'); - $encryptionEnabled = \OC_App::isEnabled('files_encryption'); - if($encryptionEnabled || $publicUploadEnabled !== 'yes') { - return new \OC_OCS_Result(null, 404, "public upload disabled by the administrator"); + if($publicUploadEnabled !== 'yes') { + return new \OC_OCS_Result(null, 403, "public upload disabled by the administrator"); } if ($share['item_type'] !== 'folder' || diff --git a/apps/files_sharing/lib/cache.php b/apps/files_sharing/lib/cache.php index 425d51113b1..1b102f9e5f8 100644 --- a/apps/files_sharing/lib/cache.php +++ b/apps/files_sharing/lib/cache.php @@ -92,12 +92,11 @@ class Shared_Cache extends Cache { } else { $query = \OC_DB::prepare( 'SELECT `fileid`, `storage`, `path`, `parent`, `name`, `mimetype`, `mimepart`,' - .' `size`, `mtime`, `encrypted`' + .' `size`, `mtime`, `encrypted`, `unencrypted_size`' .' FROM `*PREFIX*filecache` WHERE `fileid` = ?'); $result = $query->execute(array($file)); $data = $result->fetchRow(); $data['fileid'] = (int)$data['fileid']; - $data['size'] = (int)$data['size']; $data['mtime'] = (int)$data['mtime']; $data['storage_mtime'] = (int)$data['storage_mtime']; $data['encrypted'] = (bool)$data['encrypted']; @@ -106,6 +105,12 @@ class Shared_Cache extends Cache { if ($data['storage_mtime'] === 0) { $data['storage_mtime'] = $data['mtime']; } + if ($data['encrypted'] or ($data['unencrypted_size'] > 0 and $data['mimetype'] === 'httpd/unix-directory')) { + $data['encrypted_size'] = (int)$data['size']; + $data['size'] = (int)$data['unencrypted_size']; + } else { + $data['size'] = (int)$data['size']; + } return $data; } return false; @@ -259,17 +264,38 @@ class Shared_Cache extends Cache { * @return array */ public function searchByMime($mimetype) { - - if (strpos($mimetype, '/')) { - $where = '`mimetype` = ? AND '; - } else { - $where = '`mimepart` = ? AND '; + $mimepart = null; + if (strpos($mimetype, '/') === false) { + $mimepart = $mimetype; + $mimetype = null; } - $value = $this->getMimetypeId($mimetype); - - return $this->searchWithWhere($where, $value); - + // note: searchWithWhere is currently broken as it doesn't + // recurse into subdirs nor returns the correct + // file paths, so using getFolderContents() for now + + $result = array(); + $exploreDirs = array(''); + while (count($exploreDirs) > 0) { + $dir = array_pop($exploreDirs); + $files = $this->getFolderContents($dir); + // no results? + if (!$files) { + continue; + } + foreach ($files as $file) { + if ($file['mimetype'] === 'httpd/unix-directory') { + $exploreDirs[] = ltrim($dir . '/' . $file['name'], '/'); + } + else if (($mimepart && $file['mimepart'] === $mimepart) || ($mimetype && $file['mimetype'] === $mimetype)) { + // usersPath not reliable + //$file['path'] = $file['usersPath']; + $file['path'] = ltrim($dir . '/' . $file['name'], '/'); + $result[] = $file; + } + } + } + return $result; } /** @@ -313,6 +339,12 @@ class Shared_Cache extends Cache { } $row['mimetype'] = $this->getMimetype($row['mimetype']); $row['mimepart'] = $this->getMimetype($row['mimepart']); + if ($row['encrypted'] or ($row['unencrypted_size'] > 0 and $row['mimetype'] === 'httpd/unix-directory')) { + $row['encrypted_size'] = $row['size']; + $row['size'] = $row['unencrypted_size']; + } else { + $row['size'] = $row['size']; + } $files[] = $row; } } diff --git a/apps/files_sharing/lib/share/file.php b/apps/files_sharing/lib/share/file.php index c956c55a1df..ec0f368386f 100644 --- a/apps/files_sharing/lib/share/file.php +++ b/apps/files_sharing/lib/share/file.php @@ -91,10 +91,17 @@ class OC_Share_Backend_File implements OCP\Share_Backend_File_Dependent { $file['name'] = basename($item['file_target']); $file['mimetype'] = $item['mimetype']; $file['mimepart'] = $item['mimepart']; - $file['size'] = $item['size']; $file['mtime'] = $item['mtime']; $file['encrypted'] = $item['encrypted']; $file['etag'] = $item['etag']; + $storage = \OC\Files\Filesystem::getStorage('/'); + $cache = $storage->getCache(); + if ($item['encrypted'] or ($item['unencrypted_size'] > 0 and $cache->getMimetype($item['mimetype']) === 'httpd/unix-directory')) { + $file['size'] = $item['unencrypted_size']; + $file['encrypted_size'] = $item['size']; + } else { + $file['size'] = $item['size']; + } $files[] = $file; } return $files; diff --git a/apps/files_sharing/public.php b/apps/files_sharing/public.php index 4a81e482970..f03ac7205a3 100644 --- a/apps/files_sharing/public.php +++ b/apps/files_sharing/public.php @@ -137,21 +137,19 @@ if (isset($path)) { } else { OCP\Util::addScript('files', 'file-upload'); OCP\Util::addStyle('files_sharing', 'public'); + OCP\Util::addStyle('files_sharing', 'mobile'); OCP\Util::addScript('files_sharing', 'public'); OCP\Util::addScript('files', 'fileactions'); OCP\Util::addScript('files', 'jquery.iframe-transport'); OCP\Util::addScript('files', 'jquery.fileupload'); $maxUploadFilesize=OCP\Util::maxUploadFilesize($path); $tmpl = new OCP\Template('files_sharing', 'public', 'base'); - $tmpl->assign('uidOwner', $shareOwner); $tmpl->assign('displayName', \OCP\User::getDisplayName($shareOwner)); $tmpl->assign('filename', $file); $tmpl->assign('directory_path', $linkItem['file_target']); $tmpl->assign('mimetype', \OC\Files\Filesystem::getMimeType($path)); - $tmpl->assign('fileTarget', basename($linkItem['file_target'])); $tmpl->assign('dirToken', $linkItem['token']); $tmpl->assign('sharingToken', $token); - $tmpl->assign('disableSharing', true); $allowPublicUploadEnabled = (bool) ($linkItem['permissions'] & OCP\PERMISSION_CREATE); if (OC_Appconfig::getValue('core', 'shareapi_allow_public_upload', 'yes') === 'no') { $allowPublicUploadEnabled = false; @@ -159,9 +157,6 @@ if (isset($path)) { if ($linkItem['item_type'] !== 'folder') { $allowPublicUploadEnabled = false; } - $tmpl->assign('allowPublicUploadEnabled', $allowPublicUploadEnabled); - $tmpl->assign('uploadMaxFilesize', $maxUploadFilesize); - $tmpl->assign('uploadMaxHumanFilesize', OCP\Util::humanFileSize($maxUploadFilesize)); $urlLinkIdentifiers= (isset($token)?'&t='.$token:'') .(isset($_GET['dir'])?'&dir='.$_GET['dir']:'') @@ -222,17 +217,23 @@ if (isset($path)) { $maxUploadFilesize=OCP\Util::maxUploadFilesize($path); $fileHeader = (!isset($files) or count($files) > 0); $emptyContent = ($allowPublicUploadEnabled and !$fileHeader); + + $freeSpace=OCP\Util::freeSpace($path); + $uploadLimit=OCP\Util::uploadLimit(); $folder = new OCP\Template('files', 'index', ''); $folder->assign('fileList', $list->fetchPage()); $folder->assign('breadcrumb', $breadcrumbNav->fetchPage()); $folder->assign('dir', $getPath); - $folder->assign('isCreatable', false); + $folder->assign('isCreatable', $allowPublicUploadEnabled); + $folder->assign('dirToken', $linkItem['token']); $folder->assign('permissions', OCP\PERMISSION_READ); $folder->assign('isPublic',true); $folder->assign('publicUploadEnabled', 'no'); $folder->assign('files', $files); $folder->assign('uploadMaxFilesize', $maxUploadFilesize); $folder->assign('uploadMaxHumanFilesize', OCP\Util::humanFileSize($maxUploadFilesize)); + $folder->assign('freeSpace', $freeSpace); + $folder->assign('uploadLimit', $uploadLimit); // PHP upload limit $folder->assign('allowZipDownload', intval(OCP\Config::getSystemValue('allowZipDownload', true))); $folder->assign('usedSpacePercent', 0); $folder->assign('fileHeader', $fileHeader); diff --git a/apps/files_sharing/templates/public.php b/apps/files_sharing/templates/public.php index e1132142eff..3ddaf4446df 100644 --- a/apps/files_sharing/templates/public.php +++ b/apps/files_sharing/templates/public.php @@ -9,54 +9,14 @@ <input type="hidden" name="sharingToken" value="<?php p($_['sharingToken']) ?>" id="sharingToken"> <input type="hidden" name="filename" value="<?php p($_['filename']) ?>" id="filename"> <input type="hidden" name="mimetype" value="<?php p($_['mimetype']) ?>" id="mimetype"> -<header><div id="header" class="icon icon-noise"> +<header><div id="header" class="icon icon-noise <?php p((isset($_['folder']) ? 'share-folder' : 'share-file')) ?>"> <a href="<?php print_unescaped(link_to('', 'index.php')); ?>" title="" id="owncloud"><img class="svg" src="<?php print_unescaped(image_path('', 'logo-wide.svg')); ?>" alt="<?php p($theme->getName()); ?>" /></a> <div id="logo-claim" style="display:none;"><?php p($theme->getLogoClaim()); ?></div> <div class="header-right"> - <?php if (isset($_['folder'])): ?> - <span id="details"><?php p($l->t('%s shared the folder %s with you', - array($_['displayName'], $_['filename']))) ?></span> - <?php else: ?> - <span id="details"><?php p($l->t('%s shared the file %s with you', - array($_['displayName'], $_['filename']))) ?></span> - <?php endif; ?> - - - <?php if (!isset($_['folder']) || $_['allowZipDownload']): ?> - <a href="<?php p($_['downloadURL']); ?>" class="button" id="download"> - <img class="svg" alt="Download" src="<?php print_unescaped(OCP\image_path("core", "actions/download.svg")); ?>" /> - <span><?php p($l->t('Download'))?></span> - </a> - <?php endif; ?> - - <?php if ($_['allowPublicUploadEnabled']):?> - - - <input type="hidden" id="publicUploadRequestToken" name="requesttoken" value="<?php p($_['requesttoken']) ?>" /> - <input type="hidden" id="dirToken" name="dirToken" value="<?php p($_['dirToken']) ?>" /> - <input type="hidden" id="uploadMaxFilesize" name="uploadMaxFilesize" value="<?php p($_['uploadMaxFilesize']) ?>" /> - <input type="hidden" id="uploadMaxHumanFilesize" name="uploadMaxHumanFilesize" value="<?php p($_['uploadMaxHumanFilesize']) ?>" /> - <input type="hidden" id="directory_path" name="directory_path" value="<?php p($_['directory_path']) ?>" /> - <?php if($_['uploadMaxFilesize'] >= 0):?> - <input type="hidden" name="MAX_FILE_SIZE" id="max_upload" - value="<?php p($_['uploadMaxFilesize']) ?>"> - <?php endif;?> - - - <div id="data-upload-form" title="<?php p($l->t('Upload') . ' max. '.$_['uploadMaxHumanFilesize']) ?>"> - <input id="file_upload_start" type="file" name="files[]" data-url="<?php print_unescaped(OCP\Util::linkTo('files', 'ajax/upload.php')); ?>" multiple> - <a href="#" id="public_upload" class="button"> - <img class="svg" alt="Upload" src="<?php print_unescaped(OCP\image_path("core", "actions/upload.svg")); ?>" /> - <span><?php p($l->t('Upload'))?></span> - </a> - </div> - - </div> - <div> - <?php endif; ?> + <span id="details"><?php p($l->t('shared by %s', array($_['displayName']))) ?></span> </div> - </div></header> +</div></header> <div id="content"> <div id="preview"> <?php if (isset($_['folder'])): ?> @@ -72,25 +32,28 @@ <source src="<?php p($_['downloadURL']); ?>" type="<?php p($_['mimetype']); ?>" /> </video> </div> - <?php elseif (\OC\Preview::isMimeSupported($_['mimetype'])): ?> + <?php else: ?> <div id="imgframe"> - <img src="<?php p(OCP\Util::linkToRoute( 'core_ajax_public_preview', array('x' => 500, 'y' => 500, 'file' => urlencode($_['directory_path']), 't' => $_['dirToken']))); ?>" class="publicpreview"/> + <?php $size = \OC\Preview::isMimeSupported($_['mimetype']) ? 500 : 128 ?> + <img src="<?php p(OCP\Util::linkToRoute( 'core_ajax_public_preview', array('x' => $size, 'y' => $size, 'file' => urlencode($_['directory_path']), 't' => $_['dirToken']))); ?>" class="publicpreview"/> </div> - <?php else: ?> - <ul id="noPreview"> - <li class="error"> - <?php p($l->t('No preview available for').' '.$_['filename']); ?><br /> - <a href="<?php p($_['downloadURL']); ?>" id="download"><img class="svg" alt="Download" - src="<?php print_unescaped(OCP\image_path("core", "actions/download.svg")); ?>" - /><?php p($l->t('Download'))?></a> - </li> - </ul> <?php endif; ?> - <div class="directLink"><label for="directLink"><?php p($l->t('Direct link')) ?></label><input id="directLink" type="text" readonly value="<?php p($_['downloadURL']); ?>"></input></div> + <div class="directDownload"> + <a href="<?php p($_['downloadURL']); ?>" id="download" class="button"> + <img class="svg" alt="" src="<?php print_unescaped(OCP\image_path("core", "actions/download.svg")); ?>"/> + <?php p($l->t('Download %s', array($_['filename'])))?> + </a> + </div> + <div class="directLink"> + <label for="directLink"><?php p($l->t('Direct link')) ?></label> + <input id="directLink" type="text" readonly value="<?php p($_['downloadURL']); ?>"> + </div> <?php endif; ?> </div> - <footer> - <p class="info"> - <?php print_unescaped($theme->getLongFooter()); ?> - </p> - </footer> + +</div> +<footer> + <p class="info"> + <?php print_unescaped($theme->getLongFooter()); ?> + </p> +</footer> diff --git a/apps/files_sharing/tests/cache.php b/apps/files_sharing/tests/cache.php new file mode 100644 index 00000000000..56a51c83f6b --- /dev/null +++ b/apps/files_sharing/tests/cache.php @@ -0,0 +1,134 @@ +<?php +/** + * ownCloud + * + * @author Vincent Petry + * @copyright 2014 Vincent Petry <pvince81@owncloud.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + */ +require_once __DIR__ . '/base.php'; + +class Test_Files_Sharing_Cache extends Test_Files_Sharing_Base { + + function setUp() { + parent::setUp(); + + self::loginHelper(self::TEST_FILES_SHARING_API_USER1); + + // prepare user1's dir structure + $textData = "dummy file data\n"; + $this->view->mkdir('container'); + $this->view->mkdir('container/shareddir'); + $this->view->mkdir('container/shareddir/subdir'); + $this->view->mkdir('container/shareddir/emptydir'); + + $textData = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $this->view->file_put_contents('container/not shared.txt', $textData); + $this->view->file_put_contents('container/shared single file.txt', $textData); + $this->view->file_put_contents('container/shareddir/bar.txt', $textData); + $this->view->file_put_contents('container/shareddir/subdir/another.txt', $textData); + $this->view->file_put_contents('container/shareddir/subdir/another too.txt', $textData); + $this->view->file_put_contents('container/shareddir/subdir/not a text file.xml', '<xml></xml>'); + + list($this->ownerStorage, $internalPath) = $this->view->resolvePath(''); + $this->ownerCache = $this->ownerStorage->getCache(); + $this->ownerStorage->getScanner()->scan(''); + + // share "shareddir" with user2 + $fileinfo = $this->view->getFileInfo('container/shareddir'); + \OCP\Share::shareItem('folder', $fileinfo['fileid'], \OCP\Share::SHARE_TYPE_USER, + self::TEST_FILES_SHARING_API_USER2, 31); + + $fileinfo = $this->view->getFileInfo('container/shared single file.txt'); + \OCP\Share::shareItem('file', $fileinfo['fileid'], \OCP\Share::SHARE_TYPE_USER, + self::TEST_FILES_SHARING_API_USER2, 31); + + // login as user2 + self::loginHelper(self::TEST_FILES_SHARING_API_USER2); + + // retrieve the shared storage + $secondView = new \OC\Files\View('/' . self::TEST_FILES_SHARING_API_USER2); + list($this->sharedStorage, $internalPath) = $secondView->resolvePath('files/Shared/shareddir'); + $this->sharedCache = $this->sharedStorage->getCache(); + } + + function tearDown() { + $this->sharedCache->clear(); + + self::loginHelper(self::TEST_FILES_SHARING_API_USER1); + + $fileinfo = $this->view->getFileInfo('container/shareddir'); + \OCP\Share::unshare('folder', $fileinfo['fileid'], \OCP\Share::SHARE_TYPE_USER, + self::TEST_FILES_SHARING_API_USER2); + + $fileinfo = $this->view->getFileInfo('container/shared single file.txt'); + \OCP\Share::unshare('file', $fileinfo['fileid'], \OCP\Share::SHARE_TYPE_USER, + self::TEST_FILES_SHARING_API_USER2); + + $this->view->deleteAll('container'); + + $this->ownerCache->clear(); + + parent::tearDown(); + } + + /** + * Test searching by mime type + */ + function testSearchByMime() { + $results = $this->sharedStorage->getCache()->searchByMime('text'); + $check = array( + array( + 'name' => 'shared single file.txt', + 'path' => 'shared single file.txt' + ), + array( + 'name' => 'bar.txt', + 'path' => 'shareddir/bar.txt' + ), + array( + 'name' => 'another too.txt', + 'path' => 'shareddir/subdir/another too.txt' + ), + array( + 'name' => 'another.txt', + 'path' => 'shareddir/subdir/another.txt' + ), + ); + $this->verifyFiles($check, $results); + + $results2 = $this->sharedStorage->getCache()->searchByMime('text/plain'); + + $this->verifyFiles($check, $results); + } + + /** + * Checks that all provided attributes exist in the files list, + * only the values provided in $examples will be used to check against + * the file list. The files order also needs to be the same. + * + * @param array $examples array of example files + * @param array $files array of files + */ + private function verifyFiles($examples, $files) { + $this->assertEquals(count($examples), count($files)); + foreach ($files as $i => $file) { + foreach ($examples[$i] as $key => $value) { + $this->assertEquals($value, $file[$key]); + } + } + } +} diff --git a/apps/files_trashbin/ajax/preview.php b/apps/files_trashbin/ajax/preview.php index ce432f4d14e..44738734b19 100644 --- a/apps/files_trashbin/ajax/preview.php +++ b/apps/files_trashbin/ajax/preview.php @@ -34,7 +34,17 @@ try{ if ($view->is_dir($file)) { $mimetype = 'httpd/unix-directory'; } else { - $mimetype = \OC_Helper::getFileNameMimeType(pathinfo($file, PATHINFO_FILENAME)); + $pathInfo = pathinfo($file); + $fileName = $pathInfo['basename']; + // if in root dir + if ($pathInfo['dirname'] === '.') { + // cut off the .d* suffix + $i = strrpos($fileName, '.'); + if ($i !== false) { + $fileName = substr($fileName, 0, $i); + } + } + $mimetype = \OC_Helper::getFileNameMimeType($fileName); } $preview->setMimetype($mimetype); $preview->setMaxX($maxX); @@ -45,4 +55,4 @@ try{ }catch(\Exception $e) { \OC_Response::setStatus(500); \OC_Log::write('core', $e->getmessage(), \OC_Log::DEBUG); -}
\ No newline at end of file +} diff --git a/apps/user_ldap/lib/connection.php b/apps/user_ldap/lib/connection.php index c4e4efd0483..7fbabda7106 100644 --- a/apps/user_ldap/lib/connection.php +++ b/apps/user_ldap/lib/connection.php @@ -52,7 +52,7 @@ class Connection extends LDAPUtility { $this->configID = $configID; $this->configuration = new Configuration($configPrefix, !is_null($configID)); - $memcache = new \OC\Memcache\Factory(); + $memcache = \OC::$server->getMemCacheFactory(); if($memcache->isAvailable()) { $this->cache = $memcache->create(); } else { diff --git a/autotest-js.sh b/autotest-js.sh index 78f4948e7ad..8b9a106b021 100755 --- a/autotest-js.sh +++ b/autotest-js.sh @@ -33,5 +33,5 @@ then exit 2 fi -KARMA_TESTSUITE="$1" $KARMA start tests/karma.config.js --single-run +NODE_PATH='build/node_modules' KARMA_TESTSUITE="$1" $KARMA start tests/karma.config.js --single-run diff --git a/autotest.sh b/autotest.sh index 94fc692a94d..b88e9cf68b4 100755 --- a/autotest.sh +++ b/autotest.sh @@ -185,19 +185,23 @@ EOF cp $BASEDIR/tests/autoconfig-$1.php $BASEDIR/config/autoconfig.php # trigger installation - php -f index.php + echo "INDEX" + php -f index.php | grep -i -C9999 error && echo "Error during setup" && exit 101 + echo "END INDEX" #test execution echo "Testing with $1 ..." cd tests rm -rf coverage-html-$1 mkdir coverage-html-$1 - php -f enable_all.php + php -f enable_all.php | grep -i -C9999 error && echo "Error during setup" && exit 101 if [ -z "$NOCOVERAGE" ]; then $PHPUNIT --configuration phpunit-autotest.xml --log-junit autotest-results-$1.xml --coverage-clover autotest-clover-$1.xml --coverage-html coverage-html-$1 $2 $3 + RESULT=$? else echo "No coverage" $PHPUNIT --configuration phpunit-autotest.xml --log-junit autotest-results-$1.xml $2 $3 + RESULT=$? fi } diff --git a/config/config.sample.php b/config/config.sample.php index 01abc583688..ef5fb7ea5a5 100755 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -184,6 +184,13 @@ $CONFIG = array( /* Life time of a session after inactivity */ "session_lifetime" => 60 * 60 * 24, +/* + * Enable/disable session keep alive when a user is logged in in the Web UI. + * This is achieved by sending a "heartbeat" to the server to prevent + * the session timing out. + */ +"session_keepalive" => true, + /* Custom CSP policy, changing this will overwrite the standard policy */ "custom_csp_policy" => "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; frame-src *; img-src *; font-src 'self' data:; media-src *", diff --git a/core/ajax/share.php b/core/ajax/share.php index 268cd4f53a7..8b48effb458 100644 --- a/core/ajax/share.php +++ b/core/ajax/share.php @@ -121,7 +121,7 @@ if (isset($_POST['action']) && isset($_POST['itemType']) && isset($_POST['itemSo if (isset($items[0]['expiration'])) { try { $date = new DateTime($items[0]['expiration']); - $expiration = $date->format('Y-m-d'); + $expiration = $l->l('date', $date->getTimestamp()); } catch (Exception $e) { \OCP\Util::writeLog('sharing', "Couldn't read date: " . $e->getMessage(), \OCP\Util::ERROR); } @@ -187,6 +187,8 @@ if (isset($_POST['action']) && isset($_POST['itemType']) && isset($_POST['itemSo break; case 'email': + // enable l10n support + $l = OC_L10N::get('core'); // read post variables $user = OCP\USER::getUser(); $displayName = OCP\User::getDisplayName(); @@ -199,16 +201,13 @@ if (isset($_POST['action']) && isset($_POST['itemType']) && isset($_POST['itemSo if (isset($_POST['expiration']) && $_POST['expiration'] !== '') { try { $date = new DateTime($_POST['expiration']); - $expiration = $date->format('Y-m-d'); + $expiration = $l->l('date', $date->getTimestamp()); } catch (Exception $e) { \OCP\Util::writeLog('sharing', "Couldn't read date: " . $e->getMessage(), \OCP\Util::ERROR); } } - // enable l10n support - $l = OC_L10N::get('core'); - // setup the email $subject = (string)$l->t('%s shared »%s« with you', array($displayName, $file)); diff --git a/core/css/icons.css b/core/css/icons.css index 57c37c5c51c..2dc35084122 100644 --- a/core/css/icons.css +++ b/core/css/icons.css @@ -47,6 +47,10 @@ background-image: url('../img/actions/checkmark.svg'); } +.icon-checkmark-white { + background-image: url('../img/actions/checkmark-white.svg'); +} + .icon-clock { background-image: url('../img/actions/clock.svg'); } diff --git a/core/css/styles.css b/core/css/styles.css index 03eb76ddce5..bee44785f12 100644 --- a/core/css/styles.css +++ b/core/css/styles.css @@ -686,7 +686,7 @@ label.infield { cursor:text !important; top:1.05em; left:.85em; } /* Apps management as sticky footer, less obtrusive in the list */ #navigation .wrapper { min-height: 100%; - margin: 0 auto -72px; + margin: 0 auto -82px 0; } #apps-management, #navigation .push { height: 72px; diff --git a/core/img/actions/checkmark-white.png b/core/img/actions/checkmark-white.png Binary files differnew file mode 100644 index 00000000000..08b8783649f --- /dev/null +++ b/core/img/actions/checkmark-white.png diff --git a/core/img/actions/checkmark-white.svg b/core/img/actions/checkmark-white.svg new file mode 100644 index 00000000000..5e8fe8abccc --- /dev/null +++ b/core/img/actions/checkmark-white.svg @@ -0,0 +1,4 @@ +<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" version="1.1" xml:space="preserve" height="16px" viewBox="-0.5 -0.5 16 16" width="16px" enable-background="new -0.5 -0.5 16 16" y="0px" x="0px" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" overflow="visible"><metadata><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title/></cc:Work></rdf:RDF></metadata><defs> +</defs> +<path fill="#ffffff" d="M12.438,3.6875c-0.363,0-0.726,0.1314-1,0.4063l-4.5005,4.5-1.9687-2c-0.5498-0.5484-1.4489-0.5498-2,0l-0.5,0.5c-0.5512,0.5496-0.5512,1.4502,0,2l2.9687,2.9682c0.0063,0.007-0.0065,0.025,0,0.032l0.5,0.5c0.5497,0.55,1.4503,0.55,2,0l0.5-0.5,0.1875-0.219,5.313-5.2812c0.549-0.5498,0.549-1.4503,0-2l-0.5-0.5c-0.275-0.2749-0.638-0.4063-1-0.4063z" transform="translate(-0.5,-0.5)"/> +</svg> diff --git a/core/img/actions/toggle-filelist.png b/core/img/actions/toggle-filelist.png Binary files differnew file mode 100644 index 00000000000..45d363f1934 --- /dev/null +++ b/core/img/actions/toggle-filelist.png diff --git a/core/img/actions/toggle-filelist.svg b/core/img/actions/toggle-filelist.svg new file mode 100644 index 00000000000..940f6a49e63 --- /dev/null +++ b/core/img/actions/toggle-filelist.svg @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/"> + <g> + <rect rx=".5" ry=".5" height="4" width="4" y="1" x="1"/> + <rect rx=".5" ry=".5" height="1" width="9" y="2" x="6"/> + <rect rx=".5" ry=".5" height="4" width="4" y="6" x="1"/> + <rect rx=".5" ry=".5" height="1" width="9" y="7" x="6"/> + <rect rx=".5" ry=".5" height="4" width="4" y="11" x="1"/> + <rect rx=".5" ry=".5" height="1" width="9" y="12" x="6"/> + </g> +</svg> diff --git a/core/img/actions/toggle-pictures.png b/core/img/actions/toggle-pictures.png Binary files differnew file mode 100644 index 00000000000..8068d17e30d --- /dev/null +++ b/core/img/actions/toggle-pictures.png diff --git a/core/img/actions/toggle-pictures.svg b/core/img/actions/toggle-pictures.svg new file mode 100644 index 00000000000..5205c0226d1 --- /dev/null +++ b/core/img/actions/toggle-pictures.svg @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/"> + <g> + <rect rx=".5" ry=".5" height="6" width="6" y="1" x="1"/> + <rect rx=".5" ry=".5" height="6" width="6" y="1" x="9"/> + <rect rx=".5" ry=".5" height="6" width="6" y="9" x="9"/> + <rect rx=".5" ry=".5" height="6" width="6" y="9" x="1"/> + </g> +</svg> diff --git a/core/img/places/picture.png b/core/img/places/picture.png Binary files differindex 7b3af8c7f84..2b96ea518ce 100644 --- a/core/img/places/picture.png +++ b/core/img/places/picture.png diff --git a/core/img/places/picture.svg b/core/img/places/picture.svg index 791cbb5909a..82d457f5c7c 100644 --- a/core/img/places/picture.svg +++ b/core/img/places/picture.svg @@ -1,7 +1,4 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> - <g transform="translate(581.71 -2.0765)"> - <path style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m-575.01 3.0806c-0.39495 0.0765-0.70712 0.46665-0.70001 0.87488l-0.00062 26.246c0.00001 0.45808 0.41045 0.87487 0.86155 0.87488h20.28c0.4511-0.000012 0.86154-0.4168 0.86155-0.87488l0.00061-25.921c-0.00065-0.67287-0.53099-1.2037-1.0337-1.1998h-20.27zm1.2998 19.996h18l0.00082 6h-18.001z"/> - <path style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m-575.01 4.0806c-0.39495 0.0765-0.70712 0.46665-0.70001 0.87488l-0.00062 26.246c0.00001 0.45808 0.41045 0.87487 0.86155 0.87488h20.28c0.4511-0.000012 0.86154-0.4168 0.86155-0.87488l0.00061-25.921c-0.00065-0.67287-0.53099-1.2037-1.0337-1.1998h-20.27zm1.2998 19.996h18l0.00082 6h-18.001z" fill="#fff"/> - </g> + <path style="block-progression:tb;color:#000000;text-transform:none;text-indent:0" fill="#fff" d="m0.6875 4c-0.39495 0.0765-0.69461 0.4668-0.6875 0.875v22.25c0.00001 0.458 0.4239 0.875 0.875 0.875h30.25c0.4511-0.000012 0.87499-0.41692 0.875-0.875v-21.906c-0.001-0.6731-0.529-1.2229-1.031-1.219zm2.3125 3h26v10l-2-2-5 7-6.625-4-9.1875 7h-3.1875zm6 3c-1.6569 0-3 1.3431-3 3s1.3431 3 3 3 3-1.3431 3-3-1.3431-3-3-3z"/> </svg> diff --git a/core/js/config.php b/core/js/config.php index dd46f7889d1..517ea1615a8 100644 --- a/core/js/config.php +++ b/core/js/config.php @@ -55,6 +55,12 @@ $array = array( ) ), "firstDay" => json_encode($l->l('firstday', 'firstday')) , + "oc_config" => json_encode( + array( + 'session_lifetime' => \OCP\Config::getSystemValue('session_lifetime', 60 * 60 * 24), + 'session_keepalive' => \OCP\Config::getSystemValue('session_keepalive', true) + ) + ) ); // Echo it diff --git a/core/js/core.json b/core/js/core.json index 79cfc42f587..4beab7cf796 100644 --- a/core/js/core.json +++ b/core/js/core.json @@ -1,28 +1,23 @@ { + "libraries": [ + "jquery-1.10.0.min.js", + "jquery-migrate-1.2.1.min.js", + "jquery-ui-1.10.0.custom.js", + "jquery-showpassword.js", + "jquery.infieldlabel.js", + "jquery.placeholder.js", + "jquery-tipsy.js" + ], "modules": [ - "jquery-1.10.0.min.js", - "jquery-migrate-1.2.1.min.js", - "jquery-ui-1.10.0.custom.js", - "jquery-showpassword.js", - "jquery.infieldlabel.js", - "jquery.placeholder.js", - "jquery-tipsy.js", - "compatibility.js", - "jquery.ocdialog.js", - "oc-dialogs.js", - "js.js", - "octemplate.js", - "eventsource.js", - "config.js", - "multiselect.js", - "search.js", - "router.js", - "oc-requesttoken.js", - "styles.js", - "apps.js", - "fixes.js", - "jquery-ui-2.10.0.custom.js", - "jquery-tipsy.js", - "jquery.ocdialog.js" + "compatibility.js", + "jquery.ocdialog.js", + "oc-dialogs.js", + "js.js", + "octemplate.js", + "eventsource.js", + "config.js", + "multiselect.js", + "router.js", + "oc-requesttoken.js" ] } diff --git a/core/js/js.js b/core/js/js.js index e84f482d672..cb177712a3a 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -11,6 +11,8 @@ var oc_webroot; var oc_current_user = document.getElementsByTagName('head')[0].getAttribute('data-user'); var oc_requesttoken = document.getElementsByTagName('head')[0].getAttribute('data-requesttoken'); +window.oc_config = window.oc_config || {}; + if (typeof oc_webroot === "undefined") { oc_webroot = location.pathname; var pos = oc_webroot.indexOf('/index.php/'); @@ -253,6 +255,12 @@ var OC={ return link; }, /** + * Redirect to the target URL, can also be used for downloads. + */ + redirect: function(targetUrl) { + window.location = targetUrl; + }, + /** * get the absolute path to an image file * @param app the app id to which the image belongs * @param file the name of the image file @@ -364,6 +372,34 @@ var OC={ } return result; }, + + /** + * Builds a URL query from a JS map. + * @param params parameter map + * @return string containing a URL query (without question) mark + */ + buildQueryString: function(params) { + var s = ''; + var first = true; + if (!params) { + return s; + } + for (var key in params) { + var value = params[key]; + if (first) { + first = false; + } + else { + s += '&'; + } + s += encodeURIComponent(key); + if (value !== null && typeof(value) !== 'undefined') { + s += '=' + encodeURIComponent(value); + } + } + return s; + }, + /** * Opens a popup with the setting for an app. * @param appid String. The ID of the app e.g. 'calendar', 'contacts' or 'files'. @@ -708,8 +744,39 @@ function fillWindow(selector) { console.warn("This function is deprecated! Use CSS instead"); } -$(document).ready(function(){ - sessionHeartBeat(); +/** + * Initializes core + */ +function initCore() { + + /** + * Calls the server periodically to ensure that session doesn't + * time out + */ + function initSessionHeartBeat(){ + // interval in seconds + var interval = 900; + if (oc_config.session_lifetime) { + interval = Math.floor(oc_config.session_lifetime / 2); + } + // minimum one minute + if (interval < 60) { + interval = 60; + } + OC.Router.registerLoadedCallback(function(){ + var url = OC.Router.generate('heartbeat'); + setInterval(function(){ + $.post(url); + }, interval * 1000); + }); + } + + // session heartbeat (defaults to enabled) + if (typeof(oc_config.session_keepalive) === 'undefined' || + !!oc_config.session_keepalive) { + + initSessionHeartBeat(); + } if(!SVGSupport()){ //replace all svg images with png images for browser that dont support svg replaceSVG(); @@ -822,7 +889,9 @@ $(document).ready(function(){ $('input[type=text]').focus(function(){ this.select(); }); -}); +} + +$(document).ready(initCore); /** * Filter Jquery selector by attribute value @@ -952,15 +1021,3 @@ jQuery.fn.exists = function(){ return this.length > 0; }; -/** - * Calls the server periodically every 15 mins to ensure that session doesnt - * time out - */ -function sessionHeartBeat(){ - OC.Router.registerLoadedCallback(function(){ - var url = OC.Router.generate('heartbeat'); - setInterval(function(){ - $.post(url); - }, 900000); - }); -} diff --git a/core/js/tests/specHelper.js b/core/js/tests/specHelper.js index 4a30878df51..1848d08354e 100644 --- a/core/js/tests/specHelper.js +++ b/core/js/tests/specHelper.js @@ -19,6 +19,8 @@ * */ +/* global OC */ + /** * Simulate the variables that are normally set by PHP code */ @@ -57,10 +59,15 @@ window.oc_webroot = location.href + '/'; window.oc_appswebroots = { "files": window.oc_webroot + '/apps/files/' }; +window.oc_config = { + session_lifetime: 600 * 1000, + session_keepalive: false +}; // global setup for all tests (function setupTests() { - var fakeServer = null; + var fakeServer = null, + routesRequestStub; beforeEach(function() { // enforce fake XHR, tests should not depend on the server and @@ -78,9 +85,18 @@ window.oc_appswebroots = { // make it globally available, so that other tests can define // custom responses window.fakeServer = fakeServer; + + OC.Router.routes = []; + OC.Router.routes_request = { + state: sinon.stub().returns('resolved'), + done: sinon.stub() + }; }); afterEach(function() { + OC.Router.routes_request.state.reset(); + OC.Router.routes_request.done.reset(); + // uncomment this to log requests // console.log(window.fakeServer.requests); fakeServer.restore(); diff --git a/core/js/tests/specs/coreSpec.js b/core/js/tests/specs/coreSpec.js index 827669f270b..478505e9287 100644 --- a/core/js/tests/specs/coreSpec.js +++ b/core/js/tests/specs/coreSpec.js @@ -18,6 +18,8 @@ * License along with this library. If not, see <http://www.gnu.org/licenses/>. * */ + +/* global OC */ describe('Core base tests', function() { describe('Base values', function() { it('Sets webroots', function() { @@ -25,6 +27,103 @@ describe('Core base tests', function() { expect(OC.appswebroots).toBeDefined(); }); }); + describe('basename', function() { + it('Returns the nothing if no file name given', function() { + expect(OC.basename('')).toEqual(''); + }); + it('Returns the nothing if dir is root', function() { + expect(OC.basename('/')).toEqual(''); + }); + it('Returns the same name if no path given', function() { + expect(OC.basename('some name.txt')).toEqual('some name.txt'); + }); + it('Returns the base name if root path given', function() { + expect(OC.basename('/some name.txt')).toEqual('some name.txt'); + }); + it('Returns the base name if double root path given', function() { + expect(OC.basename('//some name.txt')).toEqual('some name.txt'); + }); + it('Returns the base name if subdir given without root', function() { + expect(OC.basename('subdir/some name.txt')).toEqual('some name.txt'); + }); + it('Returns the base name if subdir given with root', function() { + expect(OC.basename('/subdir/some name.txt')).toEqual('some name.txt'); + }); + it('Returns the base name if subdir given with double root', function() { + expect(OC.basename('//subdir/some name.txt')).toEqual('some name.txt'); + }); + it('Returns the base name if subdir has dot', function() { + expect(OC.basename('/subdir.dat/some name.txt')).toEqual('some name.txt'); + }); + it('Returns dot if file name is dot', function() { + expect(OC.basename('/subdir/.')).toEqual('.'); + }); + // TODO: fix the source to make it work like PHP's basename + it('Returns the dir itself if no file name given', function() { + // TODO: fix the source to make it work like PHP's dirname + // expect(OC.basename('subdir/')).toEqual('subdir'); + expect(OC.basename('subdir/')).toEqual(''); + }); + it('Returns the dir itself if no file name given with root', function() { + // TODO: fix the source to make it work like PHP's dirname + // expect(OC.basename('/subdir/')).toEqual('subdir'); + expect(OC.basename('/subdir/')).toEqual(''); + }); + }); + describe('dirname', function() { + it('Returns the nothing if no file name given', function() { + expect(OC.dirname('')).toEqual(''); + }); + it('Returns the root if dir is root', function() { + // TODO: fix the source to make it work like PHP's dirname + // expect(OC.dirname('/')).toEqual('/'); + expect(OC.dirname('/')).toEqual(''); + }); + it('Returns the root if dir is double root', function() { + // TODO: fix the source to make it work like PHP's dirname + // expect(OC.dirname('//')).toEqual('/'); + expect(OC.dirname('//')).toEqual('/'); // oh no... + }); + it('Returns dot if dir is dot', function() { + expect(OC.dirname('.')).toEqual('.'); + }); + it('Returns dot if no root given', function() { + // TODO: fix the source to make it work like PHP's dirname + // expect(OC.dirname('some dir')).toEqual('.'); + expect(OC.dirname('some dir')).toEqual('some dir'); // oh no... + }); + it('Returns the dir name if file name and root path given', function() { + // TODO: fix the source to make it work like PHP's dirname + // expect(OC.dirname('/some name.txt')).toEqual('/'); + expect(OC.dirname('/some name.txt')).toEqual(''); + }); + it('Returns the dir name if double root path given', function() { + expect(OC.dirname('//some name.txt')).toEqual('/'); // how lucky... + }); + it('Returns the dir name if subdir given without root', function() { + expect(OC.dirname('subdir/some name.txt')).toEqual('subdir'); + }); + it('Returns the dir name if subdir given with root', function() { + expect(OC.dirname('/subdir/some name.txt')).toEqual('/subdir'); + }); + it('Returns the dir name if subdir given with double root', function() { + // TODO: fix the source to make it work like PHP's dirname + // expect(OC.dirname('//subdir/some name.txt')).toEqual('/subdir'); + expect(OC.dirname('//subdir/some name.txt')).toEqual('//subdir'); // oh... + }); + it('Returns the dir name if subdir has dot', function() { + expect(OC.dirname('/subdir.dat/some name.txt')).toEqual('/subdir.dat'); + }); + it('Returns the dir name if file name is dot', function() { + expect(OC.dirname('/subdir/.')).toEqual('/subdir'); + }); + it('Returns the dir name if no file name given', function() { + expect(OC.dirname('subdir/')).toEqual('subdir'); + }); + it('Returns the dir name if no file name given with root', function() { + expect(OC.dirname('/subdir/')).toEqual('/subdir'); + }); + }); describe('Link functions', function() { var TESTAPP = 'testapp'; var TESTAPP_ROOT = OC.webroot + '/appsx/testapp'; @@ -67,4 +166,115 @@ describe('Core base tests', function() { }); }); }); + describe('Query string building', function() { + it('Returns empty string when empty params', function() { + expect(OC.buildQueryString()).toEqual(''); + expect(OC.buildQueryString({})).toEqual(''); + }); + it('Encodes regular query strings', function() { + expect(OC.buildQueryString({ + a: 'abc', + b: 'def' + })).toEqual('a=abc&b=def'); + }); + it('Encodes special characters', function() { + expect(OC.buildQueryString({ + unicode: '汉字', + })).toEqual('unicode=%E6%B1%89%E5%AD%97'); + expect(OC.buildQueryString({ + b: 'spaace value', + 'space key': 'normalvalue', + 'slash/this': 'amp&ersand' + })).toEqual('b=spaace%20value&space%20key=normalvalue&slash%2Fthis=amp%26ersand'); + }); + it('Encodes data types and empty values', function() { + expect(OC.buildQueryString({ + 'keywithemptystring': '', + 'keywithnull': null, + 'keywithundefined': null, + something: 'else' + })).toEqual('keywithemptystring=&keywithnull&keywithundefined&something=else'); + expect(OC.buildQueryString({ + 'booleanfalse': false, + 'booleantrue': true + })).toEqual('booleanfalse=false&booleantrue=true'); + expect(OC.buildQueryString({ + 'number': 123, + })).toEqual('number=123'); + }); + }); + describe('Session heartbeat', function() { + var clock, + oldConfig, + loadedStub, + routeStub, + counter; + + beforeEach(function() { + clock = sinon.useFakeTimers(); + oldConfig = window.oc_config; + loadedStub = sinon.stub(OC.Router, 'registerLoadedCallback'); + routeStub = sinon.stub(OC.Router, 'generate').returns('/heartbeat'); + counter = 0; + + fakeServer.autoRespond = true; + fakeServer.autoRespondAfter = 0; + fakeServer.respondWith(/\/heartbeat/, function(xhr) { + counter++; + xhr.respond(200, {'Content-Type': 'application/json'}, '{}'); + }); + }); + afterEach(function() { + clock.restore(); + window.oc_config = oldConfig; + loadedStub.restore(); + routeStub.restore(); + }); + it('sends heartbeat half the session lifetime when heartbeat enabled', function() { + window.oc_config = { + session_keepalive: true, + session_lifetime: 300 + }; + window.initCore(); + expect(loadedStub.calledOnce).toEqual(true); + loadedStub.yield(); + expect(routeStub.calledWith('heartbeat')).toEqual(true); + + expect(counter).toEqual(0); + + // less than half, still nothing + clock.tick(100 * 1000); + expect(counter).toEqual(0); + + // reach past half (160), one call + clock.tick(55 * 1000); + expect(counter).toEqual(1); + + // almost there to the next, still one + clock.tick(140 * 1000); + expect(counter).toEqual(1); + + // past it, second call + clock.tick(20 * 1000); + expect(counter).toEqual(2); + }); + it('does no send heartbeat when heartbeat disabled', function() { + window.oc_config = { + session_keepalive: false, + session_lifetime: 300 + }; + window.initCore(); + expect(loadedStub.notCalled).toEqual(true); + expect(routeStub.notCalled).toEqual(true); + + expect(counter).toEqual(0); + + clock.tick(1000000); + + // still nothing + expect(counter).toEqual(0); + }); + + }); }); + diff --git a/core/setup.php b/core/setup.php deleted file mode 100644 index 958376b2cce..00000000000 --- a/core/setup.php +++ /dev/null @@ -1,73 +0,0 @@ -<?php - -// Check for autosetup: -$autosetup_file = OC::$SERVERROOT."/config/autoconfig.php"; -if( file_exists( $autosetup_file )) { - OC_Log::write('core', 'Autoconfig file found, setting up owncloud...', OC_Log::INFO); - include $autosetup_file; - $_POST = array_merge ($_POST, $AUTOCONFIG); - $_REQUEST = array_merge ($_REQUEST, $AUTOCONFIG); -} - -$dbIsSet = isset($_POST['dbtype']); -$directoryIsSet = isset($_POST['directory']); -$adminAccountIsSet = isset($_POST['adminlogin']); - -if ($dbIsSet AND $directoryIsSet AND $adminAccountIsSet) { - $_POST['install'] = 'true'; - if( file_exists( $autosetup_file )) { - unlink($autosetup_file); - } -} - -OC_Util::addScript( '3rdparty', 'strengthify/jquery.strengthify' ); -OC_Util::addStyle( '3rdparty', 'strengthify/strengthify' ); -OC_Util::addScript('setup'); - -$hasSQLite = class_exists('SQLite3'); -$hasMySQL = is_callable('mysql_connect'); -$hasPostgreSQL = is_callable('pg_connect'); -$hasOracle = is_callable('oci_connect'); -$hasMSSQL = is_callable('sqlsrv_connect'); -$datadir = OC_Config::getValue('datadirectory', OC::$SERVERROOT.'/data'); -$vulnerableToNullByte = false; -if(@file_exists(__FILE__."\0Nullbyte")) { // Check if the used PHP version is vulnerable to the NULL Byte attack (CVE-2006-7243) - $vulnerableToNullByte = true; -} - -// Protect data directory here, so we can test if the protection is working -OC_Setup::protectDataDirectory(); - -$opts = array( - 'hasSQLite' => $hasSQLite, - 'hasMySQL' => $hasMySQL, - 'hasPostgreSQL' => $hasPostgreSQL, - 'hasOracle' => $hasOracle, - 'hasMSSQL' => $hasMSSQL, - 'directory' => $datadir, - 'secureRNG' => OC_Util::secureRNGAvailable(), - 'htaccessWorking' => OC_Util::isHtAccessWorking(), - 'vulnerableToNullByte' => $vulnerableToNullByte, - 'errors' => array(), - 'dbIsSet' => $dbIsSet, - 'directoryIsSet' => $directoryIsSet, -); - -if(isset($_POST['install']) AND $_POST['install']=='true') { - // We have to launch the installation process : - $e = OC_Setup::install($_POST); - $errors = array('errors' => $e); - - if(count($e) > 0) { - //OC_Template::printGuestPage("", "error", array("errors" => $errors)); - $options = array_merge($_POST, $opts, $errors); - OC_Template::printGuestPage("", "installation", $options); - } - else { - header( 'Location: '.OC_Helper::linkToRoute( 'post_setup_check' )); - exit(); - } -} -else { - OC_Template::printGuestPage("", "installation", $opts); -} diff --git a/core/setup/controller.php b/core/setup/controller.php new file mode 100644 index 00000000000..58ed4d28dc0 --- /dev/null +++ b/core/setup/controller.php @@ -0,0 +1,139 @@ +<?php +/** + * Copyright (c) 2013 Bart Visscher <bartv@thisnet.nl> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Core\Setup; + +class Controller { + public function run($post) { + // Check for autosetup: + $post = $this->loadAutoConfig($post); + $opts = $this->getSystemInfo(); + + if(isset($post['install']) AND $post['install']=='true') { + // We have to launch the installation process : + $e = \OC_Setup::install($post); + $errors = array('errors' => $e); + + if(count($e) > 0) { + $options = array_merge($post, $opts, $errors); + $this->display($options); + } + else { + $this->finishSetup(); + } + } + else { + $this->display($opts); + } + } + + public function display($post) { + $defaults = array( + 'adminlogin' => '', + 'adminpass' => '', + 'dbuser' => '', + 'dbpass' => '', + 'dbname' => '', + 'dbtablespace' => '', + 'dbhost' => '', + ); + $parameters = array_merge($defaults, $post); + + \OC_Util::addScript( '3rdparty', 'strengthify/jquery.strengthify' ); + \OC_Util::addStyle( '3rdparty', 'strengthify/strengthify' ); + \OC_Util::addScript('setup'); + \OC_Template::printGuestPage('', 'installation', $parameters); + } + + public function finishSetup() { + header( 'Location: '.\OC_Helper::linkToRoute( 'post_setup_check' )); + exit(); + } + + public function loadAutoConfig($post) { + $autosetup_file = \OC::$SERVERROOT.'/config/autoconfig.php'; + if( file_exists( $autosetup_file )) { + \OC_Log::write('core', 'Autoconfig file found, setting up owncloud...', \OC_Log::INFO); + $AUTOCONFIG = array(); + include $autosetup_file; + $post = array_merge ($post, $AUTOCONFIG); + } + + $dbIsSet = isset($post['dbtype']); + $directoryIsSet = isset($post['directory']); + $adminAccountIsSet = isset($post['adminlogin']); + + if ($dbIsSet AND $directoryIsSet AND $adminAccountIsSet) { + $post['install'] = 'true'; + if( file_exists( $autosetup_file )) { + unlink($autosetup_file); + } + } + $post['dbIsSet'] = $dbIsSet; + $post['directoryIsSet'] = $directoryIsSet; + + return $post; + } + + public function getSystemInfo() { + $hasSQLite = class_exists('SQLite3'); + $hasMySQL = is_callable('mysql_connect'); + $hasPostgreSQL = is_callable('pg_connect'); + $hasOracle = is_callable('oci_connect'); + $hasMSSQL = is_callable('sqlsrv_connect'); + $databases = array(); + if ($hasSQLite) { + $databases['sqlite'] = 'SQLite'; + } + if ($hasMySQL) { + $databases['mysql'] = 'MySQL'; + } + if ($hasPostgreSQL) { + $databases['pgsql'] = 'PostgreSQL'; + } + if ($hasOracle) { + $databases['oci'] = 'Oracle'; + } + if ($hasMSSQL) { + $databases['mssql'] = 'MS SQL'; + } + $datadir = \OC_Config::getValue('datadirectory', \OC::$SERVERROOT.'/data'); + $vulnerableToNullByte = false; + if(@file_exists(__FILE__."\0Nullbyte")) { // Check if the used PHP version is vulnerable to the NULL Byte attack (CVE-2006-7243) + $vulnerableToNullByte = true; + } + + $errors = array(); + + // Protect data directory here, so we can test if the protection is working + \OC_Setup::protectDataDirectory(); + try { + $htaccessWorking = \OC_Util::isHtAccessWorking(); + } catch (\OC\HintException $e) { + $errors[] = array( + 'error' => $e->getMessage(), + 'hint' => $e->getHint() + ); + $htaccessWorking = false; + } + + return array( + 'hasSQLite' => $hasSQLite, + 'hasMySQL' => $hasMySQL, + 'hasPostgreSQL' => $hasPostgreSQL, + 'hasOracle' => $hasOracle, + 'hasMSSQL' => $hasMSSQL, + 'databases' => $databases, + 'directory' => $datadir, + 'secureRNG' => \OC_Util::secureRNGAvailable(), + 'htaccessWorking' => $htaccessWorking, + 'vulnerableToNullByte' => $vulnerableToNullByte, + 'errors' => $errors, + ); + } +} diff --git a/core/templates/installation.php b/core/templates/installation.php index 182fc83a4d4..9670a5e9ee5 100644 --- a/core/templates/installation.php +++ b/core/templates/installation.php @@ -48,13 +48,13 @@ <legend><?php print_unescaped($l->t( 'Create an <strong>admin account</strong>' )); ?></legend> <p class="infield grouptop"> <input type="text" name="adminlogin" id="adminlogin" placeholder="" - value="<?php p(OC_Helper::init_var('adminlogin')); ?>" autocomplete="off" autofocus required /> + value="<?php p($_['adminlogin']); ?>" autocomplete="off" autofocus required /> <label for="adminlogin" class="infield"><?php p($l->t( 'Username' )); ?></label> <img class="svg" src="<?php p(image_path('', 'actions/user.svg')); ?>" alt="" /> </p> <p class="infield groupbottom"> <input type="password" name="adminpass" data-typetoggle="#show" id="adminpass" placeholder="" - value="<?php p(OC_Helper::init_var('adminpass')); ?>" required /> + value="<?php p($_['adminpass']); ?>" required /> <label for="adminpass" class="infield"><?php p($l->t( 'Password' )); ?></label> <img class="svg" id="adminpass-icon" src="<?php print_unescaped(image_path('', 'actions/password.svg')); ?>" alt="" /> <input type="checkbox" id="show" name="show" /> @@ -75,7 +75,7 @@ <label for="directory"><?php p($l->t( 'Data folder' )); ?></label> <input type="text" name="directory" id="directory" placeholder="<?php p(OC::$SERVERROOT."/data"); ?>" - value="<?php p(OC_Helper::init_var('directory', $_['directory'])); ?>" /> + value="<?php p($_['directory']); ?>" /> </div> </fieldset> <?php endif; ?> @@ -86,62 +86,16 @@ $hasOtherDB = true; else $hasOtherDB =false; //other than SQLite ?> <legend><?php p($l->t( 'Configure the database' )); ?></legend> <div id="selectDbType"> - <?php if($_['hasSQLite']): ?> - <input type='hidden' id='hasSQLite' value="true" /> - <?php if(!$hasOtherDB): ?> - <p>SQLite <?php p($l->t( 'will be used' )); ?>.</p> - <input type="hidden" id="dbtype" name="dbtype" value="sqlite" /> + <?php foreach($_['databases'] as $type => $label): ?> + <?php if(count($_['databases']) === 1): ?> + <p class="info"><?php p($label . ' ' . $l->t( 'will be used' )); ?>.</p> + <input type="hidden" id="dbtype" name="dbtype" value="<?php p($type) ?>" /> <?php else: ?> - <input type="radio" name="dbtype" value="sqlite" id="sqlite" - <?php OC_Helper::init_radio('dbtype', 'sqlite', 'sqlite'); ?>/> - <label class="sqlite" for="sqlite">SQLite</label> - <?php endif; ?> - <?php endif; ?> - - <?php if($_['hasMySQL']): ?> - <input type='hidden' id='hasMySQL' value='true'/> - <?php if(!$_['hasSQLite'] and !$_['hasPostgreSQL'] and !$_['hasOracle'] and !$_['hasMSSQL']): ?> - <p>MySQL <?php p($l->t( 'will be used' )); ?>.</p> - <input type="hidden" id="dbtype" name="dbtype" value="mysql" /> - <?php else: ?> - <input type="radio" name="dbtype" value="mysql" id="mysql" - <?php OC_Helper::init_radio('dbtype', 'mysql', 'sqlite'); ?>/> - <label class="mysql" for="mysql">MySQL</label> - <?php endif; ?> - <?php endif; ?> - - <?php if($_['hasPostgreSQL']): ?> - <?php if(!$_['hasSQLite'] and !$_['hasMySQL'] and !$_['hasOracle'] and !$_['hasMSSQL']): ?> - <p>PostgreSQL <?php p($l->t( 'will be used' )); ?>.</p> - <input type="hidden" id="dbtype" name="dbtype" value="pgsql" /> - <?php else: ?> - <label class="pgsql" for="pgsql">PostgreSQL</label> - <input type="radio" name="dbtype" value='pgsql' id="pgsql" - <?php OC_Helper::init_radio('dbtype', 'pgsql', 'sqlite'); ?>/> - <?php endif; ?> - <?php endif; ?> - - <?php if($_['hasOracle']): ?> - <?php if(!$_['hasSQLite'] and !$_['hasMySQL'] and !$_['hasPostgreSQL'] and !$_['hasMSSQL']): ?> - <p>Oracle <?php p($l->t( 'will be used' )); ?>.</p> - <input type="hidden" id="dbtype" name="dbtype" value="oci" /> - <?php else: ?> - <label class="oci" for="oci">Oracle</label> - <input type="radio" name="dbtype" value='oci' id="oci" - <?php OC_Helper::init_radio('dbtype', 'oci', 'sqlite'); ?>/> - <?php endif; ?> - <?php endif; ?> - - <?php if($_['hasMSSQL']): ?> - <input type='hidden' id='hasMSSQL' value='true'/> - <?php if(!$_['hasSQLite'] and !$_['hasMySQL'] and !$_['hasPostgreSQL'] and !$_['hasOracle']): ?> - <p>MS SQL <?php p($l->t( 'will be used' )); ?>.</p> - <input type="hidden" id="dbtype" name="dbtype" value="mssql" /> - <?php else: ?> - <label class="mssql" for="mssql">MS SQL</label> - <input type="radio" name="dbtype" value='mssql' id="mssql" <?php OC_Helper::init_radio('dbtype', 'mssql', 'sqlite'); ?>/> - <?php endif; ?> + <input type="radio" name="dbtype" value="<?php p($type) ?>" id="<?php p($type) ?>" + <?php p($_['dbtype'] === $type ? 'checked="checked" ' : '') ?>/> + <label class="<?php p($type) ?>" for="<?php p($type) ?>"><?php p($label) ?></label> <?php endif; ?> + <?php endforeach; ?> </div> <?php if($hasOtherDB): ?> @@ -149,11 +103,11 @@ <p class="infield grouptop"> <label for="dbuser" class="infield"><?php p($l->t( 'Database user' )); ?></label> <input type="text" name="dbuser" id="dbuser" placeholder="" - value="<?php p(OC_Helper::init_var('dbuser')); ?>" autocomplete="off" /> + value="<?php p($_['dbuser']); ?>" autocomplete="off" /> </p> <p class="infield groupmiddle"> <input type="password" name="dbpass" id="dbpass" placeholder="" data-typetoggle="#dbpassword" - value="<?php p(OC_Helper::init_var('dbpass')); ?>" /> + value="<?php p($_['dbpass']); ?>" /> <label for="dbpass" class="infield"><?php p($l->t( 'Database password' )); ?></label> <input type="checkbox" id="dbpassword" name="dbpassword" /> <label for="dbpassword"></label> @@ -161,7 +115,7 @@ <p class="infield groupmiddle"> <label for="dbname" class="infield"><?php p($l->t( 'Database name' )); ?></label> <input type="text" name="dbname" id="dbname" placeholder="" - value="<?php p(OC_Helper::init_var('dbname')); ?>" + value="<?php p($_['dbname']); ?>" autocomplete="off" pattern="[0-9a-zA-Z$_-]+" /> </p> <?php if($_['hasOracle']): ?> @@ -169,14 +123,14 @@ <p class="infield groupmiddle"> <label for="dbtablespace" class="infield"><?php p($l->t( 'Database tablespace' )); ?></label> <input type="text" name="dbtablespace" id="dbtablespace" placeholder="" - value="<?php p(OC_Helper::init_var('dbtablespace')); ?>" autocomplete="off" /> + value="<?php p($_['dbtablespace']); ?>" autocomplete="off" /> </p> </div> <?php endif; ?> <p class="infield groupbottom"> <label for="dbhost" class="infield"><?php p($l->t( 'Database host' )); ?></label> <input type="text" name="dbhost" id="dbhost" placeholder="" - value="<?php p(OC_Helper::init_var('dbhost')); ?>" /> + value="<?php p($_['dbhost']); ?>" /> </p> </div> <?php endif; ?> diff --git a/core/templates/layout.base.php b/core/templates/layout.base.php index 8cd237deea1..bae52a73234 100644 --- a/core/templates/layout.base.php +++ b/core/templates/layout.base.php @@ -11,7 +11,7 @@ <?php p($theme->getTitle()); ?> </title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0"> <link rel="shortcut icon" href="<?php print_unescaped(image_path('', 'favicon.png')); ?>" /> <link rel="apple-touch-icon-precomposed" href="<?php print_unescaped(image_path('', 'favicon-touch.png')); ?>" /> <?php foreach ($_['cssfiles'] as $cssfile): ?> diff --git a/core/templates/layout.guest.php b/core/templates/layout.guest.php index 47ca5903dab..6a96b17b100 100644 --- a/core/templates/layout.guest.php +++ b/core/templates/layout.guest.php @@ -11,7 +11,7 @@ <?php p($theme->getTitle()); ?> </title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0"> <meta name="apple-itunes-app" content="app-id=543672169"> <link rel="shortcut icon" href="<?php print_unescaped(image_path('', 'favicon.png')); ?>" /> <link rel="apple-touch-icon-precomposed" href="<?php print_unescaped(image_path('', 'favicon-touch.png')); ?>" /> diff --git a/core/templates/layout.user.php b/core/templates/layout.user.php index 89987625d63..bc1c700402e 100644 --- a/core/templates/layout.user.php +++ b/core/templates/layout.user.php @@ -48,15 +48,16 @@ <a href="<?php print_unescaped(link_to('', 'index.php')); ?>" title="" id="owncloud"><img class="svg" src="<?php print_unescaped(image_path('', 'logo-wide.svg')); ?>" alt="<?php p($theme->getName()); ?>" /></a> <div id="logo-claim" style="display:none;"><?php p($theme->getLogoClaim()); ?></div> - <ul id="settings" class="svg"> + <div id="settings" class="svg"> <span id="expand" tabindex="0" role="link"> <span id="expandDisplayName"><?php p(trim($_['user_displayname']) != '' ? $_['user_displayname'] : $_['user_uid']) ?></span> - <img class="svg" src="<?php print_unescaped(image_path('', 'actions/caret.svg')); ?>" /> - <?php if ($_['enableAvatars']): ?> - <div class="avatardiv"></div> - <?php endif; ?> + <img class="svg" alt="" src="<?php print_unescaped(image_path('', 'actions/caret.svg')); ?>" /> </span> + <?php if ($_['enableAvatars']): ?> + <div class="avatardiv"></div> + <?php endif; ?> <div id="expanddiv"> + <ul> <?php foreach($_['settingsnavigation'] as $entry):?> <li> <a href="<?php print_unescaped($entry['href']); ?>" title="" @@ -72,8 +73,9 @@ <?php p($l->t('Log out'));?> </a> </li> + </ul> </div> - </ul> + </div> <form class="searchbox" action="#" method="post"> <input id="searchbox" class="svg" type="search" name="query" @@ -83,37 +85,40 @@ </div></header> <nav><div id="navigation"> - <ul id="apps" class="svg"> - <div class="wrapper"><!-- for sticky footer of apps management --> + <div id="apps" class="svg"> + <ul class="wrapper"><!-- for sticky footer of apps management --> <?php foreach($_['navigation'] as $entry): ?> <li data-id="<?php p($entry['id']); ?>"> <a href="<?php print_unescaped($entry['href']); ?>" title="" <?php if( $entry['active'] ): ?> class="active"<?php endif; ?>> - <img class="icon svg" src="<?php print_unescaped($entry['icon']); ?>"/> + <img class="icon svg" alt="" src="<?php print_unescaped($entry['icon']); ?>"/> <span> <?php p($entry['name']); ?> </span> </a> </li> <?php endforeach; ?> + <?php if(OC_User::isAdminUser(OC_User::getUser())): ?> - <div class="push"></div><!-- for for sticky footer of apps management --> + <li class="push"></li><!-- for sticky footer of apps management --> <?php endif; ?> - </div> + </ul> <!-- show "More apps" link to app administration directly in app navigation, as sticky footer --> <?php if(OC_User::isAdminUser(OC_User::getUser())): ?> - <li id="apps-management"> + <ul id="apps-management"> + <li> <a href="<?php print_unescaped(OC_Helper::linkToRoute('settings_apps').'?installed'); ?>" title="" <?php if( $_['appsmanagement_active'] ): ?> class="active"<?php endif; ?>> - <img class="icon svg" src="<?php print_unescaped(OC_Helper::imagePath('settings', 'apps.svg')); ?>"/> + <img class="icon svg" alt="" src="<?php print_unescaped(OC_Helper::imagePath('settings', 'apps.svg')); ?>"/> <span> <?php p($l->t('Apps')); ?> </span> </a> </li> + </ul> <?php endif; ?> - </ul> + </div> </div></nav> <div id="content-wrapper"> diff --git a/lib/base.php b/lib/base.php index af78b4e4eb1..f2d9251294d 100644 --- a/lib/base.php +++ b/lib/base.php @@ -504,11 +504,12 @@ class OC { if (!defined('PHPUNIT_RUN')) { if (defined('DEBUG') and DEBUG) { + OC\Log\ErrorHandler::register(true); set_exception_handler(array('OC_Template', 'printExceptionErrorPage')); } else { OC\Log\ErrorHandler::register(); - OC\Log\ErrorHandler::setLogger(OC_Log::$object); } + OC\Log\ErrorHandler::setLogger(OC_Log::$object); } // register the stream wrappers @@ -690,7 +691,8 @@ class OC { // Check if ownCloud is installed or in maintenance (update) mode if (!OC_Config::getValue('installed', false)) { - require_once 'core/setup.php'; + $controller = new OC\Core\Setup\Controller(); + $controller->run($_POST); exit(); } diff --git a/lib/private/app.php b/lib/private/app.php index 0c60557914a..da09021cf3f 100644 --- a/lib/private/app.php +++ b/lib/private/app.php @@ -63,8 +63,8 @@ class OC_App{ ob_start(); foreach( $apps as $app ) { if((is_null($types) or self::isType($app, $types)) && !in_array($app, self::$loadedApps)) { - self::loadApp($app); self::$loadedApps[] = $app; + self::loadApp($app); } } ob_end_clean(); diff --git a/lib/private/connector/sabre/exceptionloggerplugin.php b/lib/private/connector/sabre/exceptionloggerplugin.php new file mode 100644 index 00000000000..8e77afaf207 --- /dev/null +++ b/lib/private/connector/sabre/exceptionloggerplugin.php @@ -0,0 +1,50 @@ +<?php + +/** + * ownCloud + * + * @author Vincent Petry + * @copyright 2014 Vincent Petry <pvince81@owncloud.com> + * + * @license AGPL3 + */ + +class OC_Connector_Sabre_ExceptionLoggerPlugin extends Sabre_DAV_ServerPlugin +{ + private $appName; + + /** + * @param string $loggerAppName app name to use when logging + */ + public function __construct($loggerAppName = 'webdav') { + $this->appName = $loggerAppName; + } + + /** + * This initializes the plugin. + * + * This function is called by Sabre_DAV_Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param Sabre_DAV_Server $server + * @return void + */ + public function initialize(Sabre_DAV_Server $server) { + + $server->subscribeEvent('exception', array($this, 'logException'), 10); + } + + /** + * Log exception + * + * @internal param Exception $e exception + */ + public function logException($e) { + $exceptionClass = get_class($e); + if ($exceptionClass !== 'Sabre_DAV_Exception_NotAuthenticated') { + \OCP\Util::logException($this->appName, $e); + } + } +} diff --git a/lib/private/connector/sabre/file.php b/lib/private/connector/sabre/file.php index c3b59007295..ed27cef440d 100644 --- a/lib/private/connector/sabre/file.php +++ b/lib/private/connector/sabre/file.php @@ -79,7 +79,7 @@ class OC_Connector_Sabre_File extends OC_Connector_Sabre_Node implements Sabre_D \OC_Log::write('webdav', '\OC\Files\Filesystem::file_put_contents() failed', \OC_Log::ERROR); $fs->unlink($partpath); // because we have no clue about the cause we can only throw back a 500/Internal Server Error - throw new Sabre_DAV_Exception(); + throw new Sabre_DAV_Exception('Could not write file contents'); } } catch (\OCP\Files\NotPermittedException $e) { // a more general case - due to whatever reason the content could not be written @@ -105,7 +105,7 @@ class OC_Connector_Sabre_File extends OC_Connector_Sabre_Node implements Sabre_D if ($renameOkay === false || $fileExists === false) { \OC_Log::write('webdav', '\OC\Files\Filesystem::rename() failed', \OC_Log::ERROR); $fs->unlink($partpath); - throw new Sabre_DAV_Exception(); + throw new Sabre_DAV_Exception('Could not rename part file to final file'); } // allow sync clients to send the mtime along in a header @@ -246,7 +246,7 @@ class OC_Connector_Sabre_File extends OC_Connector_Sabre_Node implements Sabre_D if ($fileExists) { $fs->unlink($targetPath); } - throw new Sabre_DAV_Exception(); + throw new Sabre_DAV_Exception('Could not rename part file assembled from chunks'); } // allow sync clients to send the mtime along in a header diff --git a/lib/private/connector/sabre/objecttree.php b/lib/private/connector/sabre/objecttree.php index cd3f081f7cc..d1e179af2ec 100644 --- a/lib/private/connector/sabre/objecttree.php +++ b/lib/private/connector/sabre/objecttree.php @@ -38,7 +38,20 @@ class ObjectTree extends \Sabre_DAV_ObjectTree { return $this->rootNode; } - $info = $this->getFileView()->getFileInfo($path); + if (pathinfo($path, PATHINFO_EXTENSION) === 'part') { + // read from storage + $absPath = $this->getFileView()->getAbsolutePath($path); + list($storage, $internalPath) = Filesystem::resolvePath('/' . $absPath); + if ($storage) { + $scanner = $storage->getScanner($internalPath); + // get data directly + $info = $scanner->getData($internalPath); + } + } + else { + // read from cache + $info = $this->getFileView()->getFileInfo($path); + } if (!$info) { throw new \Sabre_DAV_Exception_NotFound('File with name ' . $path . ' could not be located'); diff --git a/lib/private/helper.php b/lib/private/helper.php index 1c8d01c141f..580f81acc62 100644 --- a/lib/private/helper.php +++ b/lib/private/helper.php @@ -161,6 +161,7 @@ class OC_Helper { 'application/vnd.oasis.opendocument.text-template' => 'x-office/document', 'application/vnd.oasis.opendocument.text-web' => 'x-office/document', 'application/vnd.oasis.opendocument.text-master' => 'x-office/document', + 'application/mspowerpoint' => 'x-office/presentation', 'application/vnd.ms-powerpoint' => 'x-office/presentation', 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'x-office/presentation', 'application/vnd.openxmlformats-officedocument.presentationml.template' => 'x-office/presentation', @@ -171,6 +172,7 @@ class OC_Helper { 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12' => 'x-office/presentation', 'application/vnd.oasis.opendocument.presentation' => 'x-office/presentation', 'application/vnd.oasis.opendocument.presentation-template' => 'x-office/presentation', + 'application/msexcel' => 'x-office/spreadsheet', 'application/vnd.ms-excel' => 'x-office/spreadsheet', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'x-office/spreadsheet', 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' => 'x-office/spreadsheet', @@ -180,6 +182,7 @@ class OC_Helper { 'application/vnd.ms-excel.sheet.binary.macroEnabled.12' => 'x-office/spreadsheet', 'application/vnd.oasis.opendocument.spreadsheet' => 'x-office/spreadsheet', 'application/vnd.oasis.opendocument.spreadsheet-template' => 'x-office/spreadsheet', + 'application/msaccess' => 'database', ); if (isset($alias[$mimetype])) { @@ -445,29 +448,6 @@ class OC_Helper { * */ - //FIXME: should also check for value validation (i.e. the email is an email). - public static function init_var($s, $d = "") { - $r = $d; - if (isset($_REQUEST[$s]) && !empty($_REQUEST[$s])) { - $r = OC_Util::sanitizeHTML($_REQUEST[$s]); - } - - return $r; - } - - /** - * returns "checked"-attribute if request contains selected radio element - * OR if radio element is the default one -- maybe? - * - * @param string $s Name of radio-button element name - * @param string $v Value of current radio-button element - * @param string $d Value of default radio-button element - */ - public static function init_radio($s, $v, $d) { - if ((isset($_REQUEST[$s]) && $_REQUEST[$s] == $v) || (!isset($_REQUEST[$s]) && $v == $d)) - print "checked=\"checked\" "; - } - /** * detect if a given program is found in the search PATH * @@ -828,23 +808,39 @@ class OC_Helper { * @return number of bytes representing */ public static function maxUploadFilesize($dir) { - $upload_max_filesize = OCP\Util::computerFileSize(ini_get('upload_max_filesize')); - $post_max_size = OCP\Util::computerFileSize(ini_get('post_max_size')); - $freeSpace = \OC\Files\Filesystem::free_space($dir); - if ((int)$upload_max_filesize === 0 and (int)$post_max_size === 0) { - $maxUploadFilesize = \OC\Files\SPACE_UNLIMITED; - } elseif ((int)$upload_max_filesize === 0 or (int)$post_max_size === 0) { - $maxUploadFilesize = max($upload_max_filesize, $post_max_size); //only the non 0 value counts - } else { - $maxUploadFilesize = min($upload_max_filesize, $post_max_size); - } + return min(self::freeSpace($dir), self::uploadLimit()); + } + /** + * Calculate free space left within user quota + * + * @param $dir the current folder where the user currently operates + * @return number of bytes representing + */ + public static function freeSpace($dir) { + $freeSpace = \OC\Files\Filesystem::free_space($dir); if ($freeSpace !== \OC\Files\SPACE_UNKNOWN) { $freeSpace = max($freeSpace, 0); + return $freeSpace; + } else { + return INF; + } + } - return min($maxUploadFilesize, $freeSpace); + /** + * Calculate PHP upload limit + * + * @return PHP upload file size limit + */ + public static function uploadLimit() { + $upload_max_filesize = OCP\Util::computerFileSize(ini_get('upload_max_filesize')); + $post_max_size = OCP\Util::computerFileSize(ini_get('post_max_size')); + if ((int)$upload_max_filesize === 0 and (int)$post_max_size === 0) { + return INF; + } elseif ((int)$upload_max_filesize === 0 or (int)$post_max_size === 0) { + return max($upload_max_filesize, $post_max_size); //only the non 0 value counts } else { - return $maxUploadFilesize; + return min($upload_max_filesize, $post_max_size); } } diff --git a/lib/private/image.php b/lib/private/image.php index a6a2413f59f..62d2a1f0fff 100644 --- a/lib/private/image.php +++ b/lib/private/image.php @@ -409,14 +409,14 @@ class OC_Image { /** * @brief Loads an image from a local file. - * @param $imageref The path to a local file. + * @param $imagePath The path to a local file. * @returns An image resource or false on error */ public function loadFromFile($imagePath=false) { // exif_imagetype throws "read error!" if file is less than 12 byte if(!@is_file($imagePath) || !file_exists($imagePath) || filesize($imagePath) < 12 || !is_readable($imagePath)) { // Debug output disabled because this method is tried before loadFromBase64? - OC_Log::write('core', 'OC_Image->loadFromFile, couldn\'t load: '.$imagePath, OC_Log::DEBUG); + OC_Log::write('core', 'OC_Image->loadFromFile, couldn\'t load: ' . (string) urlencode($imagePath), OC_Log::DEBUG); return false; } $iType = exif_imagetype($imagePath); diff --git a/lib/private/l10n.php b/lib/private/l10n.php index 98665c84c55..1aa1dc5ea28 100644 --- a/lib/private/l10n.php +++ b/lib/private/l10n.php @@ -132,10 +132,10 @@ class OC_L10N implements \OCP\IL10N { $i18ndir = self::findI18nDir($app); // Localization is in /l10n, Texts are in $i18ndir // (Just no need to define date/time format etc. twice) - if((OC_Helper::issubdirectory($i18ndir.$lang.'.php', OC_App::getAppPath($app).'/l10n/') - || OC_Helper::issubdirectory($i18ndir.$lang.'.php', OC::$SERVERROOT.'/core/l10n/') + if((OC_Helper::issubdirectory($i18ndir.$lang.'.php', OC::$SERVERROOT.'/core/l10n/') || OC_Helper::issubdirectory($i18ndir.$lang.'.php', OC::$SERVERROOT.'/lib/l10n/') || OC_Helper::issubdirectory($i18ndir.$lang.'.php', OC::$SERVERROOT.'/settings') + || OC_Helper::issubdirectory($i18ndir.$lang.'.php', OC_App::getAppPath($app).'/l10n/') ) && file_exists($i18ndir.$lang.'.php')) { // Include the file, save the data from $CONFIG diff --git a/lib/private/log/errorhandler.php b/lib/private/log/errorhandler.php index 69cb960de91..1dde6b507fc 100644 --- a/lib/private/log/errorhandler.php +++ b/lib/private/log/errorhandler.php @@ -14,10 +14,23 @@ class ErrorHandler { /** @var LoggerInterface */ private static $logger; - public static function register() { + /** + * @brief remove password in URLs + * @param string $msg + * @return string + */ + protected static function removePassword($msg) { + return preg_replace('/\/\/(.*):(.*)@/', '//xxx:xxx@', $msg); + } + + public static function register($debug=false) { $handler = new ErrorHandler(); - set_error_handler(array($handler, 'onError')); + if ($debug) { + set_error_handler(array($handler, 'onAll'), E_ALL); + } else { + set_error_handler(array($handler, 'onError')); + } register_shutdown_function(array($handler, 'onShutdown')); set_exception_handler(array($handler, 'onException')); } @@ -32,14 +45,14 @@ class ErrorHandler { if($error && self::$logger) { //ob_end_clean(); $msg = $error['message'] . ' at ' . $error['file'] . '#' . $error['line']; - self::$logger->critical($msg, array('app' => 'PHP')); + self::$logger->critical(self::removePassword($msg), array('app' => 'PHP')); } } // Uncaught exception handler public static function onException($exception) { $msg = $exception->getMessage() . ' at ' . $exception->getFile() . '#' . $exception->getLine(); - self::$logger->critical($msg, array('app' => 'PHP')); + self::$logger->critical(self::removePassword($msg), array('app' => 'PHP')); } //Recoverable errors handler @@ -48,7 +61,15 @@ class ErrorHandler { return; } $msg = $message . ' at ' . $file . '#' . $line; - self::$logger->warning($msg, array('app' => 'PHP')); + self::$logger->error(self::removePassword($msg), array('app' => 'PHP')); + + } + + //Recoverable handler which catch all errors, warnings and notices + public static function onAll($number, $message, $file, $line) { + $msg = $message . ' at ' . $file . '#' . $line; + self::$logger->debug(self::removePassword($msg), array('app' => 'PHP')); } + } diff --git a/lib/private/log/owncloud.php b/lib/private/log/owncloud.php index 4c86d0e45e0..3590bbd436d 100644 --- a/lib/private/log/owncloud.php +++ b/lib/private/log/owncloud.php @@ -69,7 +69,6 @@ class OC_Log_Owncloud { } $time = new DateTime(null, $timezone); // remove username/passswords from URLs before writing the to the log file - $message = preg_replace('/\/\/(.*):(.*)@/', '//xxx:xxx@', $message); $entry=array('app'=>$app, 'message'=>$message, 'level'=>$level, 'time'=> $time->format($format)); $entry = json_encode($entry); $handle = @fopen(self::$logFile, 'a'); diff --git a/lib/private/memcache/apc.php b/lib/private/memcache/apc.php index e995cbc526e..332bbfead00 100644 --- a/lib/private/memcache/apc.php +++ b/lib/private/memcache/apc.php @@ -9,15 +9,8 @@ namespace OC\Memcache; class APC extends Cache { - /** - * entries in APC gets namespaced to prevent collisions between owncloud instances and users - */ - protected function getNameSpace() { - return $this->prefix; - } - public function get($key) { - $result = apc_fetch($this->getNamespace() . $key, $success); + $result = apc_fetch($this->getPrefix() . $key, $success); if (!$success) { return null; } @@ -25,26 +18,22 @@ class APC extends Cache { } public function set($key, $value, $ttl = 0) { - return apc_store($this->getNamespace() . $key, $value, $ttl); + return apc_store($this->getPrefix() . $key, $value, $ttl); } public function hasKey($key) { - return apc_exists($this->getNamespace() . $key); + return apc_exists($this->getPrefix() . $key); } public function remove($key) { - return apc_delete($this->getNamespace() . $key); + return apc_delete($this->getPrefix() . $key); } public function clear($prefix = '') { - $ns = $this->getNamespace() . $prefix; - $cache = apc_cache_info('user'); - foreach ($cache['cache_list'] as $entry) { - if (strpos($entry['info'], $ns) === 0) { - apc_delete($entry['info']); - } - } - return true; + $ns = $this->getPrefix() . $prefix; + $ns = preg_quote($ns, '/'); + $iter = new \APCIterator('user', '/^' . $ns . '/'); + return apc_delete($iter); } static public function isAvailable() { diff --git a/lib/private/memcache/apcu.php b/lib/private/memcache/apcu.php index dac0f5f208a..7f780f32718 100644 --- a/lib/private/memcache/apcu.php +++ b/lib/private/memcache/apcu.php @@ -9,13 +9,6 @@ namespace OC\Memcache; class APCu extends APC { - public function clear($prefix = '') { - $ns = $this->getNamespace() . $prefix; - $ns = preg_quote($ns, '/'); - $iter = new \APCIterator('user', '/^'.$ns.'/'); - return apc_delete($iter); - } - static public function isAvailable() { if (!extension_loaded('apcu')) { return false; diff --git a/lib/private/memcache/cache.php b/lib/private/memcache/cache.php index 0ad1cc7ec03..03671b3f240 100644 --- a/lib/private/memcache/cache.php +++ b/lib/private/memcache/cache.php @@ -18,7 +18,7 @@ abstract class Cache implements \ArrayAccess { * @param string $prefix */ public function __construct($prefix = '') { - $this->prefix = \OC_Util::getInstanceId() . '/' . $prefix; + $this->prefix = $prefix; } public function getPrefix() { diff --git a/lib/private/memcache/factory.php b/lib/private/memcache/factory.php index fde7d947567..334cf9a1f0e 100644 --- a/lib/private/memcache/factory.php +++ b/lib/private/memcache/factory.php @@ -8,7 +8,21 @@ namespace OC\Memcache; -class Factory { +use \OCP\ICacheFactory; + +class Factory implements ICacheFactory { + /** + * @var string $globalPrefix + */ + private $globalPrefix; + + /** + * @param string $globalPrefix + */ + public function __construct($globalPrefix) { + $this->globalPrefix = $globalPrefix; + } + /** * get a cache instance, will return null if no backend is available * @@ -16,6 +30,7 @@ class Factory { * @return \OC\Memcache\Cache */ function create($prefix = '') { + $prefix = $this->globalPrefix . '/' . $prefix; if (XCache::isAvailable()) { return new XCache($prefix); } elseif (APCu::isAvailable()) { diff --git a/lib/private/mimetypes.list.php b/lib/private/mimetypes.list.php index 08228336966..40fb1d2d97d 100644 --- a/lib/private/mimetypes.list.php +++ b/lib/private/mimetypes.list.php @@ -21,93 +21,91 @@ */ /** - * list of mimetypes by extension + * Array mapping file extensions to mimetypes (in alphabetical order). */ - return array( + 'accdb'=>'application/msaccess', + 'ai' => 'application/illustrator', + 'avi'=>'video/x-msvideo', + 'bash' => 'text/x-shellscript', + 'blend'=>'application/x-blender', + 'cc' => 'text/x-c', + 'cdr' => 'application/coreldraw', + 'cpp' => 'text/x-c++src', 'css'=>'text/css', + 'c' => 'text/x-c', + 'c++' => 'text/x-c++src', + 'doc'=>'application/msword', + 'docx'=>'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dot'=>'application/msword', + 'dotx'=>'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'dv'=>'video/dv', + 'epub' => 'application/epub+zip', + 'exe'=>'application/x-ms-dos-executable', 'flac'=>'audio/flac', 'gif'=>'image/gif', - 'gzip'=>'application/x-gzip', 'gz'=>'application/x-gzip', + 'gzip'=>'application/x-gzip', 'html'=>'text/html', 'htm'=>'text/html', - 'ics'=>'text/calendar', 'ical'=>'text/calendar', + 'ics'=>'text/calendar', + 'impress' => 'text/impress', 'jpeg'=>'image/jpeg', 'jpg'=>'image/jpeg', 'js'=>'application/javascript', + 'keynote'=>'application/x-iwork-keynote-sffkey', + 'kra'=>'application/x-krita', + 'm2t'=>'video/mp2t', + 'm4v'=>'video/mp4', + 'markdown' => 'text/markdown', + 'mdown' => 'text/markdown', + 'md' => 'text/markdown', + 'mdb'=>'application/msaccess', + 'mdwn' => 'text/markdown', + 'mobi' => 'application/x-mobipocket-ebook', + 'mov'=>'video/quicktime', + 'mp3'=>'audio/mpeg', + 'mp4'=>'video/mp4', + 'mpeg'=>'video/mpeg', + 'mpg'=>'video/mpeg', + 'msi'=>'application/x-msi', + 'numbers'=>'application/x-iwork-numbers-sffnumbers', + 'odg'=>'application/vnd.oasis.opendocument.graphics', + 'odp'=>'application/vnd.oasis.opendocument.presentation', + 'ods'=>'application/vnd.oasis.opendocument.spreadsheet', + 'odt'=>'application/vnd.oasis.opendocument.text', 'oga'=>'audio/ogg', 'ogg'=>'audio/ogg', 'ogv'=>'video/ogg', + 'pages'=>'application/x-iwork-pages-sffpages', 'pdf'=>'application/pdf', + 'php'=>'application/x-php', + 'pl'=>'application/x-pearl', 'png'=>'image/png', + 'ppt'=>'application/mspowerpoint', + 'pptx'=>'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'psd'=>'application/x-photoshop', + 'py'=>'text/x-script.python', + 'reveal' => 'text/reveal', + 'sgf' => 'application/sgf', + 'sh-lib' => 'text/x-shellscript', + 'sh' => 'text/x-shellscript', 'svg'=>'image/svg+xml', 'tar'=>'application/x-tar', - 'tgz'=>'application/x-compressed', 'tar.gz'=>'application/x-compressed', - 'tif'=>'image/tiff', + 'tgz'=>'application/x-compressed', 'tiff'=>'image/tiff', + 'tif'=>'image/tiff', 'txt'=>'text/plain', - 'zip'=>'application/zip', + 'vcard' => 'text/vcard', + 'vcf' => 'text/vcard', 'wav'=>'audio/wav', - 'odt'=>'application/vnd.oasis.opendocument.text', - 'ods'=>'application/vnd.oasis.opendocument.spreadsheet', - 'odg'=>'application/vnd.oasis.opendocument.graphics', - 'odp'=>'application/vnd.oasis.opendocument.presentation', - 'pages'=>'application/x-iwork-pages-sffpages', - 'numbers'=>'application/x-iwork-numbers-sffnumbers', - 'keynote'=>'application/x-iwork-keynote-sffkey', - 'kra'=>'application/x-krita', - 'mp3'=>'audio/mpeg', - 'doc'=>'application/msword', - 'docx'=>'application/msword', - 'xls'=>'application/msexcel', - 'xlsx'=>'application/msexcel', - 'php'=>'application/x-php', - 'exe'=>'application/x-ms-dos-executable', - 'msi'=>'application/x-msi', - 'pl'=>'application/x-pearl', - 'py'=>'application/x-python', - 'blend'=>'application/x-blender', - 'xcf'=>'application/x-gimp', - 'psd'=>'application/x-photoshop', - 'xml'=>'application/xml', - 'avi'=>'video/x-msvideo', - 'dv'=>'video/dv', - 'm2t'=>'video/mp2t', - 'mp4'=>'video/mp4', - 'm4v'=>'video/mp4', - 'mpg'=>'video/mpeg', - 'mpeg'=>'video/mpeg', - 'mov'=>'video/quicktime', 'webm'=>'video/webm', 'wmv'=>'video/x-ms-asf', - 'py'=>'text/x-script.python', - 'vcf' => 'text/vcard', - 'vcard' => 'text/vcard', - 'doc'=>'application/msword', - 'docx'=>'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'xcf'=>'application/x-gimp', 'xls'=>'application/msexcel', 'xlsx'=>'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'ppt'=>'application/mspowerpoint', - 'pptx'=>'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'sgf' => 'application/sgf', - 'cdr' => 'application/coreldraw', - 'impress' => 'text/impress', - 'ai' => 'application/illustrator', - 'epub' => 'application/epub+zip', - 'mobi' => 'application/x-mobipocket-ebook', - 'md' => 'text/markdown', - 'markdown' => 'text/markdown', - 'mdown' => 'text/markdown', - 'mdwn' => 'text/markdown', - 'reveal' => 'text/reveal', - 'c' => 'text/x-c', - 'cc' => 'text/x-c', - 'cpp' => 'text/x-c++src', - 'c++' => 'text/x-c++src', - 'sh' => 'text/x-shellscript', - 'bash' => 'text/x-shellscript', - 'sh-lib' => 'text/x-shellscript', + 'xml'=>'application/xml', + 'zip'=>'application/zip', ); diff --git a/lib/private/request.php b/lib/private/request.php index d9d5ae08e28..2c5b907846e 100755 --- a/lib/private/request.php +++ b/lib/private/request.php @@ -11,6 +11,7 @@ class OC_Request { const USER_AGENT_IE = '/MSIE/'; // Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent const USER_AGENT_ANDROID_MOBILE_CHROME = '#Android.*Chrome/[.0-9]*#'; + const USER_AGENT_FREEBOX = '#^Mozilla/5\.0$#'; /** * @brief Check overwrite condition diff --git a/lib/private/response.php b/lib/private/response.php index 04746437347..52dbb9d90f8 100644 --- a/lib/private/response.php +++ b/lib/private/response.php @@ -153,7 +153,11 @@ class OC_Response { * @param string $type disposition type, either 'attachment' or 'inline' */ static public function setContentDispositionHeader( $filename, $type = 'attachment' ) { - if (OC_Request::isUserAgent(array(OC_Request::USER_AGENT_IE, OC_Request::USER_AGENT_ANDROID_MOBILE_CHROME))) { + if (OC_Request::isUserAgent(array( + OC_Request::USER_AGENT_IE, + OC_Request::USER_AGENT_ANDROID_MOBILE_CHROME, + OC_Request::USER_AGENT_FREEBOX + ))) { header( 'Content-Disposition: ' . rawurlencode($type) . '; filename="' . rawurlencode( $filename ) . '"' ); } else { header( 'Content-Disposition: ' . rawurlencode($type) . '; filename*=UTF-8\'\'' . rawurlencode( $filename ) diff --git a/lib/private/server.php b/lib/private/server.php index 2cbd37a97d7..c9e593ec2ed 100644 --- a/lib/private/server.php +++ b/lib/private/server.php @@ -138,6 +138,10 @@ class Server extends SimpleContainer implements IServerContainer { $this->registerService('UserCache', function($c) { return new UserCache(); }); + $this->registerService('MemCacheFactory', function ($c) { + $instanceId = \OC_Util::getInstanceId(); + return new \OC\Memcache\Factory($instanceId); + }); $this->registerService('ActivityManager', function($c) { return new ActivityManager(); }); @@ -298,6 +302,15 @@ class Server extends SimpleContainer implements IServerContainer { } /** + * Returns an \OCP\CacheFactory instance + * + * @return \OCP\CacheFactory + */ + function getMemCacheFactory() { + return $this->query('MemCacheFactory'); + } + + /** * Returns the current session * * @return \OCP\ISession diff --git a/lib/private/util.php b/lib/private/util.php index 66a770a33dc..b16c5980d75 100755 --- a/lib/private/util.php +++ b/lib/private/util.php @@ -788,8 +788,12 @@ class OC_Util { } $fp = @fopen($testFile, 'w'); - @fwrite($fp, $testContent); - @fclose($fp); + if (!$fp) { + throw new OC\HintException('Can\'t create test file to check for working .htaccess file.', + 'Make sure it is possible for the webserver to write to '.$testFile); + } + fwrite($fp, $testContent); + fclose($fp); // accessing the file via http $url = OC_Helper::makeURLAbsolute(OC::$WEBROOT.'/data'.$fileName); diff --git a/lib/public/icachefactory.php b/lib/public/icachefactory.php new file mode 100644 index 00000000000..874f1ec0a59 --- /dev/null +++ b/lib/public/icachefactory.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright (c) 2014 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OCP; + +interface ICacheFactory{ + /** + * Get a memory cache instance + * + * All entries added trough the cache instance will be namespaced by $prefix to prevent collisions between apps + * + * @param string $prefix + * @return \OCP\ICache + */ + public function create($prefix = ''); + + /** + * Check if any memory cache backend is available + * + * @return bool + */ + public function isAvailable(); +} diff --git a/lib/public/iservercontainer.php b/lib/public/iservercontainer.php index b958d2d03f4..5473f3ee334 100644 --- a/lib/public/iservercontainer.php +++ b/lib/public/iservercontainer.php @@ -142,6 +142,13 @@ interface IServerContainer { function getCache(); /** + * Returns an \OCP\CacheFactory instance + * + * @return \OCP\ICacheFactory + */ + function getMemCacheFactory(); + + /** * Returns the current session * * @return \OCP\ISession diff --git a/lib/public/share.php b/lib/public/share.php index f832d04a70f..ae7d29e8b87 100644 --- a/lib/public/share.php +++ b/lib/public/share.php @@ -1152,7 +1152,7 @@ class Share { $select = '`*PREFIX*share`.`id`, `item_type`, `item_source`, `*PREFIX*share`.`parent`, `uid_owner`, ' .'`share_type`, `share_with`, `file_source`, `path`, `file_target`, ' .'`permissions`, `expiration`, `storage`, `*PREFIX*filecache`.`parent` as `file_parent`, ' - .'`name`, `mtime`, `mimetype`, `mimepart`, `size`, `encrypted`, `etag`, `mail_send`'; + .'`name`, `mtime`, `mimetype`, `mimepart`, `size`, `unencrypted_size`, `encrypted`, `etag`, `mail_send`'; } else { $select = '`*PREFIX*share`.`id`, `item_type`, `item_source`, `item_target`, `*PREFIX*share`.`parent`, `share_type`, `share_with`, `uid_owner`, diff --git a/lib/public/util.php b/lib/public/util.php index 6317f10a66f..d8497e29cfc 100644 --- a/lib/public/util.php +++ b/lib/public/util.php @@ -88,14 +88,18 @@ class Util { * @param Exception $ex exception to log */ public static function logException( $app, \Exception $ex ) { - $message = $ex->getMessage(); + $class = get_class($ex); + if ($class !== 'Exception') { + $message = $class . ': '; + } + $message .= $ex->getMessage(); if ($ex->getCode()) { $message .= ' [' . $ex->getCode() . ']'; } \OCP\Util::writeLog($app, 'Exception: ' . $message, \OCP\Util::FATAL); if (defined('DEBUG') and DEBUG) { // also log stack trace - $stack = explode('#', $ex->getTraceAsString()); + $stack = explode("\n", $ex->getTraceAsString()); // first element is empty array_shift($stack); foreach ($stack as $s) { @@ -462,4 +466,23 @@ class Util { public static function maxUploadFilesize($dir) { return \OC_Helper::maxUploadFilesize($dir); } + + /** + * Calculate free space left within user quota + * + * @param $dir the current folder where the user currently operates + * @return number of bytes representing + */ + public static function freeSpace($dir) { + return \OC_Helper::freeSpace($dir); + } + + /** + * Calculate PHP upload limit + * + * @return number of bytes representing + */ + public static function uploadLimit() { + return \OC_Helper::uploadLimit(); + } } diff --git a/settings/img/log Icon License b/settings/img/log Icon License deleted file mode 100644 index b5c3167d733..00000000000 --- a/settings/img/log Icon License +++ /dev/null @@ -1,2 +0,0 @@ -CC BY 3.0 -http://thenounproject.com/en-us/noun/printer/#icon-No109
\ No newline at end of file diff --git a/settings/img/log.png b/settings/img/log.png Binary files differdeleted file mode 100644 index b34a58f844c..00000000000 --- a/settings/img/log.png +++ /dev/null diff --git a/settings/img/log.svg b/settings/img/log.svg deleted file mode 100644 index a3939b73093..00000000000 --- a/settings/img/log.svg +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 100 100" xml:space="preserve" height="16px" viewBox="0 0 100 100" width="16px" version="1.1" y="0px" x="0px" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"><g fill="#4d4d4d"> -<rect height="19" width="4" y="45" x="75"/> -<polygon points="21 0 21 19 25 19 25 4 75 4 75 19 79 19 79 0"/> -<polygon points="75 45 75 96 44 96 44 77 25 77 25 45 21 45 21 80 41 100 79 100 79 45"/> -<rect height="3" width="38" y="45" x="31"/> -<rect height="3" width="38" y="56" x="31"/> -<rect height="3" width="38" y="67" x="31"/> -<path d="m0 21v48h19v-10h-9v-28h80v28h-9v10h19v-48h-100zm97 7h-4v-4h4v4z"/> -</g></svg> diff --git a/settings/templates/apps.php b/settings/templates/apps.php index e04815f9b99..4c77c62f511 100644 --- a/settings/templates/apps.php +++ b/settings/templates/apps.php @@ -9,9 +9,11 @@ <ul id="leftcontent" class="applist"> + <?php if(OC_Config::getValue('appstoreenabled', true) === true): ?> <li> <a class="app-external" target="_blank" href="http://owncloud.org/dev"><?php p($l->t('Add your App'));?> …</a> </li> + <?php endif; ?> <?php foreach($_['apps'] as $app):?> <li <?php if($app['active']) print_unescaped('class="active"')?> data-id="<?php p($app['id']) ?>" @@ -24,9 +26,11 @@ </li> <?php endforeach;?> + <?php if(OC_Config::getValue('appstoreenabled', true) === true): ?> <li> <a class="app-external" target="_blank" href="http://apps.owncloud.com"><?php p($l->t('More Apps'));?> …</a> </li> + <?php endif; ?> </ul> <div id="rightcontent"> <div class="appinfo"> diff --git a/tests/karma.config.js b/tests/karma.config.js index f73ade0f3c6..529bd31338f 100644 --- a/tests/karma.config.js +++ b/tests/karma.config.js @@ -29,25 +29,52 @@ * environment variable to the apps name, for example "core" or "files_encryption". * Multiple apps can be specified by separating them with space. * + * Setting the environment variable NOCOVERAGE to 1 will disable the coverage + * preprocessor, which is needed to be able to debug tests properly in a browser. */ + +/* jshint node: true */ module.exports = function(config) { + function findApps() { + /* + var fs = require('fs'); + var apps = fs.readdirSync('apps'); + return apps; + */ + // other apps tests don't run yet... needs further research / clean up + return ['files']; + } + + // respect NOCOVERAGE env variable + // it is useful to disable coverage for debugging + // because the coverage preprocessor will wrap the JS files somehow + var enableCoverage = !parseInt(process.env.NOCOVERAGE, 10); + console.log('Coverage preprocessor: ', enableCoverage?'enabled':'disabled'); + // default apps to test when none is specified (TODO: read from filesystem ?) - var defaultApps = 'core files'; - var appsToTest = process.env.KARMA_TESTSUITE || defaultApps; + var appsToTest = process.env.KARMA_TESTSUITE; + if (appsToTest) { + appsToTest = appsToTest.split(' '); + } + else { + appsToTest = ['core'].concat(findApps()); + } + + console.log('Apps to test: ', appsToTest); // read core files from core.json, // these are required by all apps so always need to be loaded // note that the loading order is important that's why they // are specified in a separate file var corePath = 'core/js/'; - var coreFiles = require('../' + corePath + 'core.json').modules; + var coreModule = require('../' + corePath + 'core.json'); var testCore = false; var files = []; var index; + var preprocessors = {}; // find out what apps to test from appsToTest - appsToTest = appsToTest.split(' '); index = appsToTest.indexOf('core'); if (index > -1) { appsToTest.splice(index, 1); @@ -60,11 +87,23 @@ module.exports = function(config) { // core mocks files.push(corePath + 'tests/specHelper.js'); - // add core files - for ( var i = 0; i < coreFiles.length; i++ ) { - files.push( corePath + coreFiles[i] ); + // add core library files + for ( var i = 0; i < coreModule.libraries.length; i++ ) { + var srcFile = corePath + coreModule.libraries[i]; + files.push(srcFile); + } + + // add core modules files + for ( var i = 0; i < coreModule.modules.length; i++ ) { + var srcFile = corePath + coreModule.modules[i]; + files.push(srcFile); + if (enableCoverage) { + preprocessors[srcFile] = 'coverage'; + } } + // TODO: settings pages + // need to test the core app as well ? if (testCore) { // core tests @@ -73,7 +112,11 @@ module.exports = function(config) { for ( var i = 0; i < appsToTest.length; i++ ) { // add app JS - files.push('apps/' + appsToTest[i] + '/js/*.js'); + var srcFile = 'apps/' + appsToTest[i] + '/js/*.js'; + files.push(srcFile); + if (enableCoverage) { + preprocessors[srcFile] = 'coverage'; + } // add test specs files.push('apps/' + appsToTest[i] + '/tests/js/*.js'); } @@ -83,7 +126,6 @@ module.exports = function(config) { // base path, that will be used to resolve files and exclude basePath: '..', - // frameworks to use frameworks: ['jasmine'], @@ -106,9 +148,7 @@ module.exports = function(config) { // web server port port: 9876, - preprocessors: { - 'apps/files/js/*.js': 'coverage' - }, + preprocessors: preprocessors, coverageReporter: { dir:'tests/karma-coverage', diff --git a/tests/lib/errorHandler.php b/tests/lib/errorHandler.php new file mode 100644 index 00000000000..68b87deccb6 --- /dev/null +++ b/tests/lib/errorHandler.php @@ -0,0 +1,62 @@ +<?php +/** + * ownCloud + * + * @author Bjoern Schiessle + * @copyright 2014 Bjoern Schiessle <schiessle@owncloud.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + */ + +class Test_ErrorHandler extends \PHPUnit_Framework_TestCase { + + /** + * @brief provide username, password combinations for testRemovePassword + * @return array + */ + function passwordProvider() { + return array( + array('user', 'password'), + array('user@owncloud.org', 'password'), + array('user', 'pass@word'), + array('us:er', 'password'), + array('user', 'pass:word'), + ); + + } + + /** + * @dataProvider passwordProvider + * @param string $username + * @param string $password + */ + function testRemovePassword($username, $password) { + $url = 'http://'.$username.':'.$password.'@owncloud.org'; + $expectedResult = 'http://xxx:xxx@owncloud.org'; + $result = TestableErrorHandler::testRemovePassword($url); + + $this->assertEquals($expectedResult, $result); + } + +} + +/** + * @brief dummy class to access protected methods of \OC\Log\ErrorHandler + */ +class TestableErrorHandler extends \OC\Log\ErrorHandler { + public static function testRemovePassword($msg) { + return self::removePassword($msg); + } +} diff --git a/tests/lib/request.php b/tests/lib/request.php index c6401a57144..1d77acc70ae 100644 --- a/tests/lib/request.php +++ b/tests/lib/request.php @@ -118,6 +118,21 @@ class Test_Request extends PHPUnit_Framework_TestCase { ), true ), + array( + 'Mozilla/5.0 (X11; Linux i686; rv:24.0) Gecko/20100101 Firefox/24.0', + OC_Request::USER_AGENT_FREEBOX, + false + ), + array( + 'Mozilla/5.0', + OC_Request::USER_AGENT_FREEBOX, + true + ), + array( + 'Fake Mozilla/5.0', + OC_Request::USER_AGENT_FREEBOX, + false + ), ); } } |