diff --git a/apps/files/css/files.css b/apps/files/css/files.css index 748bc2e63a1748060d9631993c3f70f3f7fab5b9..ced2006ec06a666206c142c162d4f93def7c0bdd 100644 --- a/apps/files/css/files.css +++ b/apps/files/css/files.css @@ -23,7 +23,9 @@ #new>ul>li>p { cursor:pointer; } #new>ul>li>form>input { padding:0.3em; margin:-0.3em; } -#upload { +#trash { height:17px; margin:0 0 0 1em; z-index:1010; position:absolute; right:13.5em; } + +#upload { height:27px; padding:0; margin-left:0.2em; overflow:hidden; } #upload a { @@ -109,6 +111,7 @@ table td.filename form { font-size:.85em; margin-left:3em; margin-right:3em; } } #fileList .fileactions a.action img { position:relative; top:.2em; } #fileList a.action { display:inline; margin:-.5em 0; padding:1em .5em 1em .5em !important; } +#fileList img.move2trash { display:inline; margin:-.5em 0; padding:1em .5em 1em .5em !important; float:right; } a.action.delete { float:right; } a.action>img { max-height:16px; max-width:16px; vertical-align:text-bottom; } .selectedActions { display:none; float:right; } diff --git a/apps/files/index.php b/apps/files/index.php index d2cd76a71066516b25c11fa9ef186fd2108ee089..7e767cc8a4ff3bb390ecc9fd1c6472bf7bef2361 100644 --- a/apps/files/index.php +++ b/apps/files/index.php @@ -131,9 +131,10 @@ if ($needUpgrade) { $tmpl->assign('isCreatable', \OC\Files\Filesystem::isCreatable($dir . '/')); $tmpl->assign('permissions', $permissions); $tmpl->assign('files', $files); + $tmpl->assign('trash', \OCP\App::isEnabled('files_trashbin')); $tmpl->assign('uploadMaxFilesize', $maxUploadFilesize); $tmpl->assign('uploadMaxHumanFilesize', OCP\Util::humanFileSize($maxUploadFilesize)); $tmpl->assign('allowZipDownload', intval(OCP\Config::getSystemValue('allowZipDownload', true))); $tmpl->assign('usedSpacePercent', (int)$storageInfo['relative']); $tmpl->printPage(); -} +} \ No newline at end of file diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js index f5ee363a4c82e7fd2da9107c6105a03eb98482f5..c30f1bcddd8396feaeb194d65a28441db35e4010 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -147,15 +147,19 @@ $(document).ready(function () { } else { var downloadScope = 'file'; } - 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()); - }); - + + 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()); + }); + } + $('#fileList tr').each(function(){ FileActions.display($(this).children('td.filename')); }); + }); FileActions.register('all', 'Delete', OC.PERMISSION_DELETE, function () { @@ -185,6 +189,7 @@ FileActions.register('all', 'Rename', OC.PERMISSION_UPDATE, function () { FileList.rename(filename); }); + FileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename) { window.location = OC.linkTo('files', 'index.php') + '?dir=' + encodeURIComponent($('#dir').val()).replace(/%2F/g, '/') + '/' + encodeURIComponent(filename); }); diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 04b7d92e2c35065af78f9a98dc8565c19e9d88fe..d0810f70145e520f37ce62279d595f5583ec437b 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -271,65 +271,39 @@ var FileList={ } }, do_delete:function(files){ + if(files.substr){ + files=[files]; + } + for (var i in files) { + var deleteAction = $('tr').filterAttr('data-file',files[i]).children("td.date").children(".action.delete"); + var oldHTML = deleteAction[0].outerHTML; + var newHTML = '<img class="move2trash" data-action="Delete" title="'+t('files', 'perform delete operation')+'" src="'+ OC.imagePath('core', 'loading.gif') +'"></a>'; + deleteAction[0].outerHTML = newHTML; + } // Finish any existing actions if (FileList.lastAction) { FileList.lastAction(); } - FileList.prepareDeletion(files); - - if (!FileList.useUndo) { - FileList.lastAction(); - } else { - // NOTE: Temporary fix to change the text to unshared for files in root of Shared folder - if ($('#dir').val() == '/Shared') { - OC.Notification.showHtml(t('files', 'unshared {files}', {'files': escapeHTML(files)})+'<span class="undo">'+t('files', 'undo')+'</span>'); - } else { - OC.Notification.showHtml(t('files', 'deleted {files}', {'files': escapeHTML(files)})+'<span class="undo">'+t('files', 'undo')+'</span>'); - } - } - }, - finishDelete:function(ready,sync){ - if(!FileList.deleteCanceled && FileList.deleteFiles){ - var fileNames=JSON.stringify(FileList.deleteFiles); - $.ajax({ - url: OC.filePath('files', 'ajax', 'delete.php'), - async:!sync, - type:'post', - data: {dir:$('#dir').val(),files:fileNames}, - complete: function(data){ - boolOperationFinished(data, function(){ - OC.Notification.hide(); - $.each(FileList.deleteFiles,function(index,file){ - FileList.remove(file); + var fileNames = JSON.stringify(files); + $.post(OC.filePath('files', 'ajax', 'delete.php'), + {dir:$('#dir').val(),files:fileNames}, + function(result){ + if (result.status == 'success') { + $.each(files,function(index,file){ + var files = $('tr').filterAttr('data-file',file); + files.hide(); + files.find('input[type="checkbox"]').removeAttr('checked'); + files.removeClass('selected'); }); - FileList.deleteCanceled=true; - FileList.deleteFiles=null; - FileList.lastAction = null; - if(ready){ - ready(); - } - }); - } - }); - } - }, - prepareDeletion:function(files){ - if(files.substr){ - files=[files]; - } - $.each(files,function(index,file){ - var files = $('tr').filterAttr('data-file',file); - files.hide(); - files.find('input[type="checkbox"]').removeAttr('checked'); - files.removeClass('selected'); - }); - procesSelection(); - FileList.deleteCanceled=false; - FileList.deleteFiles=files; - FileList.lastAction = function() { - FileList.finishDelete(null, true); - }; + procesSelection(); + } else { + $.each(files,function(index,file) { + var deleteAction = $('tr').filterAttr('data-file',file).children("td.date").children(".move2trash"); + deleteAction[0].outerHTML = oldHTML; + }); + } + }); } }; diff --git a/apps/files/js/files.js b/apps/files/js/files.js index 8e0399b0e0c21509b8b623f794bd9df76e290465..3d09d6aa2b65e9921b3b16b17e89e0fbd282503d 100644 --- a/apps/files/js/files.js +++ b/apps/files/js/files.js @@ -114,6 +114,11 @@ $(document).ready(function() { $(this).parent().children('#file_upload_start').trigger('click'); return false; }); + + // Show trash bin + $('#trash a').live('click', function() { + window.location=OC.filePath('files_trashbin', '', 'index.php'); + }); var lastChecked; diff --git a/apps/files/templates/index.php b/apps/files/templates/index.php index b66b523ae38e2a7c5f4ce97880145ab33e51a5a5..2d4ed9ab2d9b33012db04917c041885caa3ab7e6 100644 --- a/apps/files/templates/index.php +++ b/apps/files/templates/index.php @@ -35,6 +35,11 @@ <a href="#" class="svg" onclick="return false;"></a> </form> </div> + <?php if ($_['trash'] ): ?> + <div id="trash" class="button"> + <a><?php echo $l->t('Trash');?></a> + </div> + <?php endif; ?> <div id="uploadprogresswrapper"> <div id="uploadprogressbar"></div> <input type="button" class="stop" style="display:none" @@ -42,7 +47,6 @@ onclick="javascript:Files.cancelUploads();" /> </div> - </div> <div id="file_action_panel"></div> <?php else:?> diff --git a/apps/files_trashbin/ajax/undelete.php b/apps/files_trashbin/ajax/undelete.php new file mode 100644 index 0000000000000000000000000000000000000000..a7bb5b9de2dfc969fa7e05e57f77b2b4434f13c6 --- /dev/null +++ b/apps/files_trashbin/ajax/undelete.php @@ -0,0 +1,45 @@ +<?php + +if(!OCP\User::isLoggedIn()) { + exit; +} + +$files = $_REQUEST['files']; +$dirlisting = $_REQUEST['dirlisting']; +$list = explode(';', $files); + +$error = array(); +$success = array(); + +$i = 0; +foreach ($list as $file) { + if ( $dirlisting=='0') { + $delimiter = strrpos($file, '.d'); + $filename = substr($file, 0, $delimiter); + $timestamp = substr($file, $delimiter+2); + } else { + $path_parts = pathinfo($file); + $filename = $path_parts['basename']; + $timestamp = null; + } + + if ( !OCA_Trash\Trashbin::restore($file, $filename, $timestamp) ) { + $error[] = $filename; + } else { + $success[$i]['filename'] = $file; + $success[$i]['timestamp'] = $timestamp; + $i++; + } + +} + +if ( $error ) { + $filelist = ''; + foreach ( $error as $e ) { + $filelist .= $e.', '; + } + OCP\JSON::error(array("data" => array("message" => "Couldn't restore ".rtrim($filelist,', '), "success" => $success, "error" => $error))); +} else { + OCP\JSON::success(array("data" => array("success" => $success))); +} + diff --git a/apps/files_trashbin/appinfo/app.php b/apps/files_trashbin/appinfo/app.php new file mode 100644 index 0000000000000000000000000000000000000000..3741d42c781a08848c14eeaae383ed74e880382f --- /dev/null +++ b/apps/files_trashbin/appinfo/app.php @@ -0,0 +1,7 @@ +<?php + +OC::$CLASSPATH['OCA_Trash\Hooks'] = 'apps/files_trashbin/lib/hooks.php'; +OC::$CLASSPATH['OCA_Trash\Trashbin'] = 'apps/files_trashbin/lib/trash.php'; + + +OCP\Util::connectHook('OC_Filesystem', 'delete', "OCA_Trash\Hooks", "remove_hook"); diff --git a/apps/files_trashbin/appinfo/database.xml b/apps/files_trashbin/appinfo/database.xml new file mode 100644 index 0000000000000000000000000000000000000000..1144a1c9a974a1fd45af02620b5898062d929ccb --- /dev/null +++ b/apps/files_trashbin/appinfo/database.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="ISO-8859-1" ?> +<database> + + <name>*dbname*</name> + <create>true</create> + <overwrite>false</overwrite> + + <charset>utf8</charset> + + <table> + + <name>*dbprefix*files_trash</name> + + <declaration> + + <field> + <name>id</name> + <type>text</type> + <default></default> + <notnull>true</notnull> + <length>50</length> + </field> + + <field> + <name>user</name> + <type>text</type> + <default></default> + <notnull>true</notnull> + <length>50</length> + </field> + + <field> + <name>timestamp</name> + <type>text</type> + <default></default> + <notnull>true</notnull> + <length>12</length> + </field> + + <field> + <name>location</name> + <type>text</type> + <default></default> + <notnull>true</notnull> + <length>200</length> + </field> + + <field> + <name>type</name> + <type>text</type> + <default></default> + <notnull>true</notnull> + <length>4</length> + </field> + + <field> + <name>mime</name> + <type>text</type> + <default></default> + <notnull>true</notnull> + <length>30</length> + </field> + + <index> + <name>id_index</name> + <field> + <name>id</name> + <sorting>ascending</sorting> + </field> + </index> + + <index> + <name>timestamp_index</name> + <field> + <name>timestamp</name> + <sorting>ascending</sorting> + </field> + </index> + + <index> + <name>user_index</name> + <field> + <name>user</name> + <sorting>ascending</sorting> + </field> + </index> + + </declaration> + + </table> + +</database> diff --git a/apps/files_trashbin/appinfo/info.xml b/apps/files_trashbin/appinfo/info.xml new file mode 100644 index 0000000000000000000000000000000000000000..9b486126361b58fa662d35de74489f2cd13d3a95 --- /dev/null +++ b/apps/files_trashbin/appinfo/info.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<info> + <id>files_trashbin</id> + <name>Trash</name> + <description>Trash bin</description> + <licence>AGPL</licence> + <author>Bjoern Schiessle</author> + <shipped>true</shipped> + <require>4.9</require> + <default_enable/> + <types> + <filesystem/> + </types> +</info> diff --git a/apps/files_trashbin/appinfo/version b/apps/files_trashbin/appinfo/version new file mode 100644 index 0000000000000000000000000000000000000000..49d59571fbf6e077eece30f8c418b6aad15e20b0 --- /dev/null +++ b/apps/files_trashbin/appinfo/version @@ -0,0 +1 @@ +0.1 diff --git a/apps/files_trashbin/download.php b/apps/files_trashbin/download.php new file mode 100644 index 0000000000000000000000000000000000000000..665697dca5f37527980e54a4cae148543d7460da --- /dev/null +++ b/apps/files_trashbin/download.php @@ -0,0 +1,51 @@ +<?php + +/** +* ownCloud - trash bin +* +* @author Bjoern Schiessle +* @copyright 2013 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/>. +* +*/ + +// Check if we are a user +OCP\User::checkLoggedIn(); + +$filename = $_GET["file"]; + +$view = new OC_FilesystemView('/'.\OCP\User::getUser().'/files_trashbin'); + +if(!$view->file_exists($filename)) { + header("HTTP/1.0 404 Not Found"); + $tmpl = new OCP\Template( '', '404', 'guest' ); + $tmpl->assign('file', $filename); + $tmpl->printPage(); + exit; +} + +$ftype=$view->getMimeType( $filename ); + +header('Content-Type:'.$ftype);if ( preg_match( "/MSIE/", $_SERVER["HTTP_USER_AGENT"] ) ) { + header( 'Content-Disposition: attachment; filename="' . rawurlencode( basename($filename) ) . '"' ); +} else { + header( 'Content-Disposition: attachment; filename*=UTF-8\'\'' . rawurlencode( basename($filename) ) + . '; filename="' . rawurlencode( basename($filename) ) . '"' ); +} +OCP\Response::disableCaching(); +header('Content-Length: '. $view->filesize($filename)); + +OC_Util::obEnd(); +$view->readfile( $filename ); diff --git a/apps/files_trashbin/index.php b/apps/files_trashbin/index.php new file mode 100644 index 0000000000000000000000000000000000000000..46a601cfdde60523138e5f2c7181b4b1b53453b1 --- /dev/null +++ b/apps/files_trashbin/index.php @@ -0,0 +1,100 @@ +<?php + +// Check if we are a user +OCP\User::checkLoggedIn(); + +OCP\Util::addScript('files_trashbin', 'trash'); +OCP\Util::addScript('files_trashbin', 'disableDefaultActions'); +OCP\Util::addScript('files', 'fileactions'); +$tmpl = new OCP\Template('files_trashbin', 'index', 'user'); + +$user = \OCP\User::getUser(); +$view = new OC_Filesystemview('/'.$user.'/files_trashbin'); + +OCP\Util::addStyle('files', 'files'); +OCP\Util::addScript('files', 'filelist'); + +$dir = isset($_GET['dir']) ? stripslashes($_GET['dir']) : ''; + +if ($dir) { + $dirlisting = true; + $view = new \OC_FilesystemView('/'.\OCP\User::getUser().'/files_trashbin'); + $fullpath = \OCP\Config::getSystemValue('datadirectory').$view->getAbsolutePath($dir); + $dirContent = opendir($fullpath); + $i = 0; + while($entryName = readdir($dirContent)) { + if ( $entryName != '.' && $entryName != '..' ) { + $pos = strpos($dir.'/', '/', 1); + $tmp = substr($dir, 0, $pos); + $pos = strrpos($tmp, '.d'); + $timestamp = substr($tmp,$pos+2); + $result[] = array( + 'id' => $entryName, + 'timestamp' => $timestamp, + 'mime' => $view->getMimeType($dir.'/'.$entryName), + 'type' => $view->is_dir($dir.'/'.$entryName) ? 'dir' : 'file', + 'location' => $dir, + ); + } + } + closedir($fullpath); + +} else { + $dirlisting = false; + $query = \OC_DB::prepare('SELECT id,location,timestamp,type,mime FROM *PREFIX*files_trash WHERE user=?'); + $result = $query->execute(array($user))->fetchAll(); +} + +$files = array(); +foreach ($result as $r) { + $i = array(); + $i['name'] = $r['id']; + $i['date'] = OCP\Util::formatDate($r['timestamp']); + $i['timestamp'] = $r['timestamp']; + $i['mimetype'] = $r['mime']; + $i['type'] = $r['type']; + if ($i['type'] == 'file') { + $fileinfo = pathinfo($r['id']); + $i['basename'] = $fileinfo['filename']; + $i['extension'] = isset($fileinfo['extension']) ? ('.'.$fileinfo['extension']) : ''; + } + $i['directory'] = $r['location']; + if ($i['directory'] == '/') { + $i['directory'] = ''; + } + $i['permissions'] = OCP\PERMISSION_READ; + $files[] = $i; +} + +// Make breadcrumb +$breadcrumb = array(array('dir' => '', 'name' => 'Trash')); +$pathtohere = ''; +foreach (explode('/', $dir) as $i) { + if ($i != '') { + if ( preg_match('/^(.+)\.d[0-9]+$/', $i, $match) ) { + $name = $match[1]; + } else { + $name = $i; + } + $pathtohere .= '/' . $i; + $breadcrumb[] = array('dir' => $pathtohere, 'name' => $name); + } +} + +$breadcrumbNav = new OCP\Template('files', 'part.breadcrumb', ''); +$breadcrumbNav->assign('breadcrumb', $breadcrumb, false); +$breadcrumbNav->assign('baseURL', OCP\Util::linkTo('files_trashbin', 'index.php') . '?dir=', false); + +$list = new OCP\Template('files_trashbin', 'part.list', ''); +$list->assign('files', $files, false); +$list->assign('baseURL', OCP\Util::linkTo('files_trashbin', 'index.php'). '?dir='.$dir, false); +$list->assign('downloadURL', OCP\Util::linkTo('files_trashbin', 'download.php') . '?file='.$dir, false); +$list->assign('disableSharing', true); +$list->assign('dirlisting', $dirlisting); +$list->assign('disableDownloadActions', true); +$tmpl->assign('breadcrumb', $breadcrumbNav->fetchPage(), false); +$tmpl->assign('fileList', $list->fetchPage(), false); +$tmpl->assign('files', $files); +$tmpl->assign('dir', OC_Filesystem::normalizePath($view->getAbsolutePath())); + +$tmpl->printPage(); diff --git a/apps/files_trashbin/js/disableDefaultActions.js b/apps/files_trashbin/js/disableDefaultActions.js new file mode 100644 index 0000000000000000000000000000000000000000..56b95407dd34cb105f90f8f00776dfffd5b185f5 Binary files /dev/null and b/apps/files_trashbin/js/disableDefaultActions.js differ diff --git a/apps/files_trashbin/js/trash.js b/apps/files_trashbin/js/trash.js new file mode 100644 index 0000000000000000000000000000000000000000..3ad0ab04fb9ae653982e3c26765b00e7c4998d3a Binary files /dev/null and b/apps/files_trashbin/js/trash.js differ diff --git a/apps/files_trashbin/lib/hooks.php b/apps/files_trashbin/lib/hooks.php new file mode 100644 index 0000000000000000000000000000000000000000..d3bee105b510ca2974d109e57362d4924a01d733 --- /dev/null +++ b/apps/files_trashbin/lib/hooks.php @@ -0,0 +1,45 @@ +<?php +/** + * ownCloud - trash bin + * + * @author Bjoern Schiessle + * @copyright 2013 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/>. + * + */ + +/** + * This class contains all hooks. + */ + +namespace OCA_Trash; + +class Hooks { + + /** + * @brief Copy files to trash bin + * @param array + * + * This function is connected to the delete signal of OC_Filesystem + * to copy the file to the trash bin + */ + public static function remove_hook($params) { + + if ( \OCP\App::isEnabled('files_trashbin') ) { + $path = $params['path']; + Trashbin::move2trash($path); + } + } +} diff --git a/apps/files_trashbin/lib/trash.php b/apps/files_trashbin/lib/trash.php new file mode 100644 index 0000000000000000000000000000000000000000..a7eff3d44e0bdf1539e2b6f8515242d2b328427e --- /dev/null +++ b/apps/files_trashbin/lib/trash.php @@ -0,0 +1,264 @@ +<?php +/** + * ownCloud - trash bin + * + * @author Bjoern Schiessle + * @copyright 2013 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/>. + * + */ + +namespace OCA_Trash; + +class Trashbin { + + const DEFAULT_RETENTION_OBLIGATION=180; // how long do we keep files in the trash bin if no other value is defined in the config file (unit: days) + /** + * move file to the trash bin + * + * @param $file_path path to the deleted file/directory relative to the files root directory + */ + public static function move2trash($file_path) { + $user = \OCP\User::getUser(); + $view = new \OC_FilesystemView('/'. $user); + if (!$view->is_dir('files_trashbin')) { + $view->mkdir('files_trashbin'); + $view->mkdir("versions_trashbin"); + } + + $path_parts = pathinfo($file_path); + + $deleted = $path_parts['basename']; + $location = $path_parts['dirname']; + $timestamp = time(); + $mime = $view->getMimeType('files'.$file_path); + + if ( $view->is_dir('files'.$file_path) ) { + $type = 'dir'; + } else { + $type = 'file'; + } + + self::copy_recursive($file_path, 'files_trashbin/'.$deleted.'.d'.$timestamp, $view); + + if ( $view->file_exists('files_trashbin/'.$deleted.'.d'.$timestamp) ) { + $query = \OC_DB::prepare("INSERT INTO *PREFIX*files_trash (id,timestamp,location,type,mime,user) VALUES (?,?,?,?,?,?)"); + $result = $query->execute(array($deleted, $timestamp, $location, $type, $mime, $user)); + if ( !$result ) { // if file couldn't be added to the database than also don't store it in the trash bin. + $view->deleteAll('files_trashbin/'.$deleted.'.d'.$timestamp); + \OC_Log::write('files_trashbin', 'trash bin database couldn\'t be updated', \OC_log::ERROR); + return; + } + + if ( \OCP\App::isEnabled('files_versions') ) { + if ( $view->is_dir('files_versions'.$file_path) ) { + $view->rename('files_versions'.$file_path, 'versions_trashbin/'. $deleted.'.d'.$timestamp); + } else if ( $versions = \OCA_Versions\Storage::getVersions($file_path) ) { + foreach ($versions as $v) { + $view->rename('files_versions'.$v['path'].'.v'.$v['version'], 'versions_trashbin/'. $deleted.'.v'.$v['version'].'.d'.$timestamp); + } + } + } + } else { + \OC_Log::write('files_trashbin', 'Couldn\'t move '.$file_path.' to the trash bin' , \OC_log::ERROR); + } + + self::expire(); + } + + + /** + * restore files from trash bin + * @param $file path to the deleted file + * @param $filename name of the file + * @param $timestamp time when the file was deleted + */ + public static function restore($file, $filename, $timestamp) { + + $user = \OCP\User::getUser(); + $view = new \OC_FilesystemView('/'.$user); + + if ( $timestamp ) { + $query = \OC_DB::prepare('SELECT location,type FROM *PREFIX*files_trash WHERE user=? AND id=? AND timestamp=?'); + $result = $query->execute(array($user,$filename,$timestamp))->fetchAll(); + if ( count($result) != 1 ) { + \OC_Log::write('files_trashbin', 'trash bin database inconsistent!', \OC_Log::ERROR); + return false; + } + + // if location no longer exists, restore file in the root directory + $location = $result[0]['location']; + if ( $result[0]['location'] != '/' && + (!$view->is_dir('files'.$result[0]['location']) || + !$view->isUpdatable('files'.$result[0]['location'])) ) { + $location = ''; + } + } else { + $path_parts = pathinfo($filename); + $result[] = array( + 'location' => $path_parts['dirname'], + 'type' => $view->is_dir('/files_trashbin/'.$file) ? 'dir' : 'files', + ); + $location = ''; + } + + $source = \OC_Filesystem::normalizePath('files_trashbin/'.$file); + $target = \OC_Filesystem::normalizePath('files/'.$location.'/'.$filename); + + // we need a extension in case a file/dir with the same name already exists + $ext = self::getUniqueExtension($location, $filename, $view); + $mtime = $view->filemtime($source); + if( $view->rename($source, $target.$ext) ) { + $view->touch($target.$ext, $mtime); + // if versioning app is enabled, copy versions from the trash bin back to the original location + if ( \OCP\App::isEnabled('files_versions') ) { + if ( $result[0]['type'] == 'dir' ) { + $view->rename(\OC_Filesystem::normalizePath('versions_trashbin/'. $file), \OC_Filesystem::normalizePath('files_versions/'.$location.'/'.$filename.$ext)); + } else if ( $versions = self::getVersionsFromTrash($file, $timestamp) ) { + foreach ($versions as $v) { + if ($timestamp ) { + $view->rename('versions_trashbin/'.$filename.'.v'.$v.'.d'.$timestamp, 'files_versions/'.$location.'/'.$filename.$ext.'.v'.$v); + } else { + $view->rename('versions_trashbin/'.$file.'.v'.$v, 'files_versions/'.$location.'/'.$filename.$ext.'.v'.$v); + } + } + } + } + + if ( $timestamp ) { + $query = \OC_DB::prepare('DELETE FROM *PREFIX*files_trash WHERE user=? AND id=? AND timestamp=?'); + $query->execute(array($user,$filename,$timestamp)); + } + + return true; + } else { + \OC_Log::write('files_trashbin', 'Couldn\'t restore file from trash bin, '.$filename , \OC_log::ERROR); + } + + return false; + } + + /** + * clean up the trash bin + */ + private static function expire() { + + $view = new \OC_FilesystemView('/'.\OCP\User::getUser()); + $user = \OCP\User::getUser(); + + $query = \OC_DB::prepare('SELECT location,type,id,timestamp FROM *PREFIX*files_trash WHERE user=?'); + $result = $query->execute(array($user))->fetchAll(); + + $retention_obligation = \OC_Config::getValue('trashbin_retention_obligation', self::DEFAULT_RETENTION_OBLIGATION); + + $limit = time() - ($retention_obligation * 86400); + + foreach ( $result as $r ) { + $timestamp = $r['timestamp']; + $filename = $r['id']; + if ( $r['timestamp'] < $limit ) { + $view->unlink('files_trashbin/'.$filename.'.d'.$timestamp); + if ($r['type'] == 'dir') { + $view->unlink('versions_trashbin/'.$filename.'.d'.$timestamp); + } else if ( $versions = self::getVersionsFromTrash($filename, $timestamp) ) { + foreach ($versions as $v) { + $view->unlink('versions_trashbin/'.$filename.'.v'.$v.'.d'.$timestamp); + } + } + } + } + + $query = \OC_DB::prepare('DELETE FROM *PREFIX*files_trash WHERE user=? AND timestamp<?'); + $query->execute(array($user,$limit)); + } + + /** + * recursive copy to copy a whole directory + * + * @param $source source path, relative to the users files directory + * @param $destination destination path relative to the users root directoy + * @param $view file view for the users root directory + */ + private static function copy_recursive( $source, $destination, $view ) { + if ( $view->is_dir( 'files'.$source ) ) { + $view->mkdir( $destination ); + $view->touch($destination, $view->filemtime('files'.$source)); + foreach ( \OC_Files::getDirectoryContent($source) as $i ) { + $pathDir = $source.'/'.$i['name']; + if ( $view->is_dir('files'.$pathDir) ) { + self::copy_recursive($pathDir, $destination.'/'.$i['name'], $view); + } else { + $view->copy( 'files'.$pathDir, $destination . '/' . $i['name'] ); + $view->touch($destination . '/' . $i['name'], $view->filemtime('files'.$pathDir)); + } + } + } else { + $view->copy( 'files'.$source, $destination ); + $view->touch($destination, $view->filemtime('files'.$source)); + } + } + + /** + * find all versions which belong to the file we want to restore + * @param $filename name of the file which should be restored + * @param $timestamp timestamp when the file was deleted + */ + private static function getVersionsFromTrash($filename, $timestamp) { + $view = new \OC_FilesystemView('/'.\OCP\User::getUser().'/versions_trashbin'); + $versionsName = \OCP\Config::getSystemValue('datadirectory').$view->getAbsolutePath($filename); + $versions = array(); + + if ($timestamp ) { + // fetch for old versions + $matches = glob( $versionsName.'.v*.d'.$timestamp ); + $offset = -strlen($timestamp)-2; + } else { + $matches = glob( $versionsName.'.v*' ); + } + + foreach( $matches as $ma ) { + if ( $timestamp ) { + $parts = explode( '.v', substr($ma, 0, $offset) ); + $versions[] = ( end( $parts ) ); + } else { + $parts = explode( '.v', $ma ); + $versions[] = ( end( $parts ) ); + } + } + return $versions; + } + + /** + * find unique extension for restored file if a file with the same name already exists + * @param $location where the file should be restored + * @param $filename name of the file + * @param $view filesystem view relative to users root directory + * @return string with unique extension + */ + private static function getUniqueExtension($location, $filename, $view) { + $ext = ''; + if ( $view->file_exists('files'.$location.'/'.$filename) ) { + $tmpext = '.restored'; + $ext = $tmpext; + $i = 1; + while ( $view->file_exists('files'.$location.'/'.$filename.$ext) ) { + $ext = $tmpext.$i; + $i++; + } + } + return $ext; + } + +} diff --git a/apps/files_trashbin/templates/index.php b/apps/files_trashbin/templates/index.php new file mode 100644 index 0000000000000000000000000000000000000000..c3e51b4becdfe4d5aad5e30a61c508453416000f --- /dev/null +++ b/apps/files_trashbin/templates/index.php @@ -0,0 +1,34 @@ +<!--[if IE 8]><style>input[type="checkbox"]{padding:0;}table td{position:static !important;}</style><![endif]--> +<div id="controls"> + <?php echo($_['breadcrumb']); ?> + <div id="file_action_panel"></div> +</div> +<div id='notification'></div> + +<?php if (isset($_['files']) && count($_['files'])==0):?> + <div id="emptyfolder"><?php echo $l->t('Nothing in here. Your trash bin is empty!')?></div> +<?php endif; ?> + +<table> + <thead> + <tr> + <th id='headerName'> + <input type="checkbox" id="select_all" /> + <span class='name'><?php echo $l->t( 'Name' ); ?></span> + <span class='selectedActions'> + <a href="" class="undelete"> + <img class="svg" alt="<?php echo $l->t( 'Restore' ); ?>" + src="<?php echo OCP\image_path("core", "actions/undelete.png"); ?>" /> + <?php echo $l->t('Restore')?> + </a> + </span> + </th> + <th id="headerDate"> + <span id="modified"><?php echo $l->t( 'Deleted' ); ?></span> + </th> + </tr> + </thead> + <tbody id="fileList"> + <?php echo($_['fileList']); ?> + </tbody> +</table> diff --git a/apps/files_trashbin/templates/part.list.php b/apps/files_trashbin/templates/part.list.php new file mode 100644 index 0000000000000000000000000000000000000000..fe8a71f44e6514c3ee04af38a310fca3c3e97d34 --- /dev/null +++ b/apps/files_trashbin/templates/part.list.php @@ -0,0 +1,76 @@ +<input type="hidden" id="disableSharing" data-status="<?php echo $_['disableSharing']; ?>"> +<?php foreach($_['files'] as $file): + $simple_file_size = OCP\simple_file_size($file['size']); + // the bigger the file, the darker the shade of grey; megabytes*2 + $simple_size_color = intval(200-$file['size']/(1024*1024)*2); + if($simple_size_color<0) $simple_size_color = 0; + $relative_deleted_date = OCP\relative_modified_date($file['timestamp']); + // the older the file, the brighter the shade of grey; days*14 + $relative_date_color = round((time()-$file['mtime'])/60/60/24*14); + if($relative_date_color>200) $relative_date_color = 200; + $name = str_replace('+', '%20', urlencode($file['name'])); + $name = str_replace('%2F', '/', $name); + $directory = str_replace('+', '%20', urlencode($file['directory'])); + $directory = str_replace('%2F', '/', $directory); ?> + <tr data-filename="<?php echo $file['name'];?>" + data-type="<?php echo ($file['type'] == 'dir')?'dir':'file'?>" + data-mime="<?php echo $file['mimetype']?>" + data-permissions='<?php echo $file['permissions']; ?>' + <?php if ( $_['dirlisting'] ): ?> + id="<?php echo $file['directory'].'/'.$file['name'];?>" + data-file="<?php echo $file['directory'].'/'.$file['name'];?>" + data-timestamp='' + data-dirlisting=1 + <?php else: ?> + id="<?php echo $file['name'].'.d'.$file['timestamp'];?>" + data-file="<?php echo $file['name'].'.d'.$file['timestamp'];?>" + data-timestamp='<?php echo $file['timestamp'];?>' + data-dirlisting=0 + <?php endif; ?>> + <td class="filename svg" + <?php if($file['type'] == 'dir'): ?> + style="background-image:url(<?php echo OCP\mimetype_icon('dir'); ?>)" + <?php else: ?> + style="background-image:url(<?php echo OCP\mimetype_icon($file['mimetype']); ?>)" + <?php endif; ?> + > + <?php if(!isset($_['readonly']) || !$_['readonly']): ?><input type="checkbox" /><?php endif; ?> + <?php if($file['type'] == 'dir'): ?> + <?php if( $_['dirlisting'] ): ?> + <a class="name" href="<?php echo $_['baseURL'].'/'.$name; ?>" title=""> + <?php else: ?> + <a class="name" href="<?php echo $_['baseURL'].'/'.$name.'.d'.$file['timestamp']; ?>" title=""> + <?php endif; ?> + <?php else: ?> + <?php if( $_['dirlisting'] ): ?> + <a class="name" href="<?php echo $_['downloadURL'].'/'.$name; ?>" title=""> + <?php else: ?> + <a class="name" href="<?php echo $_['downloadURL'].'/'.$name.'.d'.$file['timestamp'];?>" title=""> + <?php endif; ?> + <?php endif; ?> + <span class="nametext"> + <?php if($file['type'] == 'dir'):?> + <?php echo htmlspecialchars($file['name']);?> + <?php else:?> + <?php echo htmlspecialchars($file['basename']);?><span + class='extension'><?php echo $file['extension'];?></span> + <?php endif;?> + </span> + <?php if($file['type'] == 'dir'):?> + <span class="uploadtext" currentUploads="0"> + </span> + <?php endif;?> + </a> + </td> + <td class="date"> + <span class="modified" + title="<?php echo $file['date']; ?>" + style="color:rgb(<?php echo $relative_date_color.',' + .$relative_date_color.',' + .$relative_date_color ?>)"> + <?php echo $relative_deleted_date; ?> + </span> + </td> + </tr> +<?php endforeach; + \ No newline at end of file diff --git a/apps/files_versions/appinfo/app.php b/apps/files_versions/appinfo/app.php index afc0a67edbac4024d373b27dc6f915052e19e9fe..edd0a2f70226a5c5cfb1c0a05362342830d2f2be 100644 --- a/apps/files_versions/appinfo/app.php +++ b/apps/files_versions/appinfo/app.php @@ -12,5 +12,5 @@ OCP\Util::addscript('files_versions', 'versions'); // Listen to write signals OCP\Util::connectHook('OC_Filesystem', 'write', "OCA_Versions\Hooks", "write_hook"); // Listen to delete and rename signals -OCP\Util::connectHook('OC_Filesystem', 'delete', "OCA_Versions\Hooks", "remove_hook"); +OCP\Util::connectHook('OC_Filesystem', 'post-delete', "OCA_Versions\Hooks", "remove_hook"); OCP\Util::connectHook('OC_Filesystem', 'rename', "OCA_Versions\Hooks", "rename_hook"); \ No newline at end of file diff --git a/config/config.sample.php b/config/config.sample.php index 78d513c7f23d044f53de2245fafaab3f2cb7992a..05663a09a46b4d22fbca6289b9165a07a43514d1 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -102,6 +102,9 @@ $CONFIG = array( /* Password to use for sendmail mail, depends on mail_smtpauth if this is used */ "mail_smtppassword" => "", +/* How long should ownCloud keep deleted files in the trash bin, default value: 180 days */ +'trashbin_retention_obligation' => 180, + /* Check 3rdparty apps for malicious code fragments */ "appcodechecker" => "", diff --git a/core/img/actions/undelete.png b/core/img/actions/undelete.png new file mode 100644 index 0000000000000000000000000000000000000000..d712527ef617dca921bcf75b917a1f728bc2102c Binary files /dev/null and b/core/img/actions/undelete.png differ