From 53dee843dee8ebd219188e98b5f8ff8f29a752b3 Mon Sep 17 00:00:00 2001 From: pavel Date: Sun, 14 Aug 2016 21:12:27 +0300 Subject: [PATCH 1/3] - server part is separated from client part to a greater extent; - removed `fileConnector` option from server-side; - removed 'Preview' and 'Thumbnail' params from `getinfo` response; - all preview paths (images, media, office files) are now associated to connector. Absolute paths for icons exclusively; - all preview paths are now built at the client-side (connector independent) for better API support; - added support of seeking for media files in PHP connector (including S3 storage plugin); - PHP connector refactored, following OOP practices; - added icons for OpenOffice files; --- connectors/php/BaseFilemanager.php | 90 +------ connectors/php/LocalFilemanager.php | 181 +++++++------- connectors/php/application/Fm.php | 59 +++++ connectors/php/application/FmApplication.php | 250 +++++++++++++++++++ connectors/php/application/Logger.php | 99 ++++++++ connectors/php/application/facade/Log.php | 14 ++ connectors/php/config.php | 4 + connectors/php/filemanager.php | 3 +- connectors/php/plugins/s3/S3Filemanager.php | 120 ++++----- images/fileicons/odp.png | Bin 0 -> 13654 bytes images/fileicons/ods.png | Bin 0 -> 12338 bytes images/fileicons/odt.png | Bin 0 -> 11770 bytes scripts/filemanager.config.default.json | 3 +- scripts/filemanager.js | 202 ++++++++++----- scripts/filemanager.min.js | 4 +- 15 files changed, 724 insertions(+), 305 deletions(-) create mode 100644 connectors/php/application/Fm.php create mode 100644 connectors/php/application/FmApplication.php create mode 100644 connectors/php/application/Logger.php create mode 100644 connectors/php/application/facade/Log.php create mode 100644 images/fileicons/odp.png create mode 100644 images/fileicons/ods.png create mode 100644 images/fileicons/odt.png diff --git a/connectors/php/BaseFilemanager.php b/connectors/php/BaseFilemanager.php index 755ba895..ade91154 100644 --- a/connectors/php/BaseFilemanager.php +++ b/connectors/php/BaseFilemanager.php @@ -1,4 +1,6 @@ '', 'File Type' => '', 'Protected' => 0, - 'Thumbnail' => '', - 'Preview' => '', 'Error' => '', 'Code' => 0, 'Properties' => array( @@ -65,10 +63,10 @@ public function __construct($extraConfig) if(isset($_REQUEST['config'])) { $this->getvar('config'); if (file_exists($this->fm_path . "/scripts/" . $_REQUEST['config'])) { - $this->__log('Loading ' . basename($this->get['config']) . ' config file.'); + Log::info('Loading ' . basename($this->get['config']) . ' config file.'); $content = file_get_contents($this->fm_path . "/scripts/" . basename($this->get['config'])); } else { - $this->__log($this->get['config'] . ' config file does not exists.'); + Log::info($this->get['config'] . ' config file does not exists.'); $this->error("Given config file (".basename($this->get['config']).") does not exist !"); } } else { @@ -88,19 +86,6 @@ public function __construct($extraConfig) if(!empty($extraConfig)) { $this->setup($extraConfig); } - - // set logfile path according to system if not set into config file - if(!isset($this->config['options']['logfile'])) { - $this->config['options']['logfile'] = sys_get_temp_dir() . '/filemanager.log'; - } - - // Log actions or not? - if ($this->config['options']['logger'] == true ) { - if(isset($this->config['options']['logfile'])) { - $this->logfile = $this->config['options']['logfile']; - } - $this->enableLog(); - } } /** @@ -182,8 +167,7 @@ abstract function download($force); abstract function getimage($thumbnail); /** - * Read file data - filemanager action - * Intended to read and output file contents when it's not possible to get file by direct URL (e.g. protected file). + * Read and output file contents data - filemanager action * Initially implemented for viewing audio/video/docs/pdf and other files hosted on AWS S3 remote server. * @see S3Filemanager::readfile() */ @@ -312,7 +296,7 @@ public function handleRequest() } echo json_encode($response); - die(); + exit; } /** @@ -327,10 +311,10 @@ public function error($string) 'Properties' => $this->defaultInfo['Properties'], ); - $this->__log('error message: "' . $string . '"', 2); + Log::info('error message: "' . $string . '"'); echo json_encode($array); - die(); + exit; } /** @@ -347,64 +331,6 @@ public function lang($string) } } - /** - * Write log to file - * @param string $msg - * @param int $traceLevel - */ - protected function __log($msg, $traceLevel = 1) - { - if($this->logger == true) { - $backtrace = debug_backtrace(); - $entry = $backtrace[$traceLevel]; - $info = "{$entry['class']}::{$entry['function']}()"; - - $fp = fopen($this->logfile, "a"); - $str = "[" . date("d/m/Y h:i:s", time()) . "]#". $this->get_user_ip() . "#" . $info . " - " . $msg; - fwrite($fp, $str . PHP_EOL); - fclose($fp); - } - } - - public function enableLog($logfile = '') - { - $this->logger = true; - - if($logfile != '') { - $this->logfile = $logfile; - } - - $this->__log(__METHOD__ . ' - Log enabled (in '. $this->logfile. ' file)'); - } - - public function disableLog() - { - $this->logger = false; - - $this->__log(__METHOD__ . ' - Log disabled'); - } - - /** - * Return user IP address - * @return mixed - */ - protected function get_user_ip() - { - $client = @$_SERVER['HTTP_CLIENT_IP']; - $forward = @$_SERVER['HTTP_X_FORWARDED_FOR']; - $remote = $_SERVER['REMOTE_ADDR']; - - if (filter_var($client, FILTER_VALIDATE_IP)) { - $ip = $client; - } elseif (filter_var($forward, FILTER_VALIDATE_IP)) { - $ip = $forward; - } else { - $ip = $remote; - } - - return $ip; - } - /** * Retrieve data from $_GET global var * @param string $var diff --git a/connectors/php/LocalFilemanager.php b/connectors/php/LocalFilemanager.php index 0aef7256..82f83d61 100644 --- a/connectors/php/LocalFilemanager.php +++ b/connectors/php/LocalFilemanager.php @@ -21,7 +21,6 @@ class LocalFilemanager extends BaseFilemanager protected $allowed_actions = array(); protected $doc_root; protected $path_to_files; - protected $connector_script_url; protected $dynamic_fileroot = 'userfiles'; public function __construct($extraConfig = array()) @@ -48,18 +47,10 @@ public function __construct($extraConfig = array()) } $this->path_to_files = $this->cleanPath($this->path_to_files); - // set path to the connector script file - if($this->config['options']['fileConnector']) { - $this->connector_script_url = $this->config['options']['fileConnector']; - } else { - $script_path = str_replace($_SERVER['DOCUMENT_ROOT'], '', $_SERVER['SCRIPT_FILENAME']); - $this->connector_script_url = $this->cleanPath('/' . $script_path); - } - - $this->__log('$this->fm_path: "' . $this->fm_path . '"'); - $this->__log('$this->path_to_files: "' . $this->path_to_files . '"'); - $this->__log('$this->doc_root: "' . $this->doc_root . '"'); - $this->__log('$this->dynamic_fileroot: "' . $this->dynamic_fileroot . '"'); + Log::info('$this->fm_path: "' . $this->fm_path . '"'); + Log::info('$this->path_to_files: "' . $this->path_to_files . '"'); + Log::info('$this->doc_root: "' . $this->doc_root . '"'); + Log::info('$this->dynamic_fileroot: "' . $this->dynamic_fileroot . '"'); $this->setParams(); $this->setPermissions(); @@ -81,13 +72,13 @@ public function setFileRoot($path, $mkdir = false) $this->path_to_files = $this->cleanPath($path); } - $this->__log('Overwritten with setFileRoot() method:'); - $this->__log('$this->path_to_files: "' . $this->path_to_files . '"'); - $this->__log('$this->dynamic_fileroot: "' . $this->dynamic_fileroot . '"'); + Log::info('Overwritten with setFileRoot() method:'); + Log::info('$this->path_to_files: "' . $this->path_to_files . '"'); + Log::info('$this->dynamic_fileroot: "' . $this->dynamic_fileroot . '"'); if($mkdir && !file_exists($this->path_to_files)) { mkdir($this->path_to_files, 0755, true); - $this->__log('creating "' . $this->path_to_files . '" folder through mkdir()'); + Log::info('creating "' . $this->path_to_files . '" folder through mkdir()'); } } @@ -122,7 +113,7 @@ public function getfolder() $files_list = array(); $current_path = $this->getFullPath($this->get['path'], true); - $this->__log('opening folder "' . $current_path . '"'); + Log::info('opening folder "' . $current_path . '"'); if(!is_dir($current_path)) { $this->error(sprintf($this->lang('DIRECTORY_NOT_EXIST'), $this->get['path'])); @@ -174,7 +165,7 @@ public function getinfo() $current_path = $this->getFullPath($path, true); $filename = basename($current_path); - $this->__log('opening file "' . $current_path . '"'); + Log::info('opening file "' . $current_path . '"'); // check if file is readable if(!$this->has_system_permission($current_path, array('r'))) { @@ -196,7 +187,7 @@ public function add() { $current_path = $this->getFullPath($this->post['currentpath'], true); - $this->__log('uploading to "' . $current_path . '"'); + Log::info('uploading to "' . $current_path . '"'); // check if file is writable if(!$this->has_system_permission($current_path, array('w'))) { @@ -224,7 +215,7 @@ public function addfolder() $new_dir = $this->normalizeString($this->get['name']); $new_path = $current_path . $new_dir; - $this->__log('adding folder "' . $new_path . '"'); + Log::info('adding folder "' . $new_path . '"'); if(is_dir($new_path)) { $this->error(sprintf($this->lang('DIRECTORY_ALREADY_EXISTS'), $this->get['name'])); @@ -263,7 +254,7 @@ public function rename() $old_file = $this->getFullPath($this->get['old'], true) . $suffix; $new_file = $this->getFullPath($newPath, true) . '/' . $newName . $suffix; - $this->__log('renaming "' . $old_file . '" to "' . $new_file . '"'); + Log::info('renaming "' . $old_file . '" to "' . $new_file . '"'); if(!$this->has_permission('rename')) { $this->error(sprintf($this->lang('NOT_ALLOWED'))); @@ -305,7 +296,7 @@ public function rename() $this->error(sprintf($this->lang('ERROR_RENAMING_FILE'), $filename, $newName)); } } else { - $this->__log('renamed "' . $old_file . '" to "' . $new_file . '"'); + Log::info('renamed "' . $old_file . '" to "' . $new_file . '"'); // for image only - rename thumbnail if original image was successfully renamed if(!is_dir($new_file)) { @@ -346,7 +337,7 @@ public function move() $newFullPath = $newPath . $filename . $suffix; $isDirOldPath = is_dir($oldPath); - $this->__log('moving "' . $oldPath . '" to "' . $newFullPath . '"'); + Log::info('moving "' . $oldPath . '" to "' . $newFullPath . '"'); // check if file is writable if(!$this->has_system_permission($oldPath, array('w')) || !$this->has_system_permission($newPath, array('w'))) { @@ -389,7 +380,7 @@ public function move() $this->error(sprintf($this->lang('ERROR_RENAMING_FILE'), $filename, $this->get['new'])); } } else { - $this->__log('moved "' . $oldPath . '" to "' . $newFullPath . '"'); + Log::info('moved "' . $oldPath . '" to "' . $newFullPath . '"'); // move thumbnail file or thumbnails folder if exists if(file_exists($old_thumbnail)) { @@ -422,7 +413,7 @@ public function replace() $old_path = $this->getFullPath($this->post['newfilepath']); $upload_dir = dirname($old_path) . '/'; - $this->__log('replacing "' . $old_path . '"'); + Log::info('replacing "' . $old_path . '"'); if(!$this->has_permission('replace') || !$this->has_permission('upload')) { $this->error(sprintf($this->lang('NOT_ALLOWED'))); @@ -449,7 +440,7 @@ public function replace() // success upload if(!property_exists($result['files'][0], 'error')) { $new_path = $upload_dir . $result['files'][0]->name; - $this->__log('replacing "' . $old_path . '" with "' . $new_path . '"'); + Log::info('replacing "' . $old_path . '" with "' . $new_path . '"'); rename($new_path, $old_path); @@ -471,7 +462,7 @@ public function editfile() { $current_path = $this->getFullPath($this->get['path'], true); - $this->__log('opening "' . $current_path . '"'); + Log::info('opening "' . $current_path . '"'); // check if file is writable if(!$this->has_system_permission($current_path, array('w'))) { @@ -506,7 +497,7 @@ public function savefile() { $current_path = $this->getFullPath($this->post['path'], true); - $this->__log('saving "' . $current_path . '"'); + Log::info('saving "' . $current_path . '"'); if(!$this->has_permission('edit') || !$this->is_editable($current_path)) { $this->error(sprintf($this->lang('NOT_ALLOWED'))); @@ -523,7 +514,7 @@ public function savefile() $this->error(sprintf($this->lang('ERROR_SAVING_FILE'))); } - $this->__log('saved "' . $current_path . '"'); + Log::info('saved "' . $current_path . '"'); $array = array( 'Error' => "", @@ -535,21 +526,67 @@ public function savefile() } /** + * Seekable stream: http://stackoverflow.com/a/23046071/1789808 * @inheritdoc - * Local connector is able to reach all files directly, but it is used to preview files placed outside server root - * @see BaseFilemanager::readfile() to get purpose description */ public function readfile() { $current_path = $this->getFullPath($this->get['path'], true); + $filesize = filesize($current_path); + $length = $filesize; + $offset = 0; + + if(isset($_SERVER['HTTP_RANGE'])) { + if(!preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches)) { + header('HTTP/1.1 416 Requested Range Not Satisfiable'); + header('Content-Range: bytes */' . $filesize); + exit; + } - header('Content-type: ' . mime_content_type($current_path)); + $offset = intval($matches[1]); + + if(isset($matches[2])) { + $end = intval($matches[2]); + if($offset > $end) { + header('HTTP/1.1 416 Requested Range Not Satisfiable'); + header('Content-Range: bytes */' . $filesize); + exit; + } + $length = $end - $offset; + } else { + $length = $filesize - $offset; + } + + $bytes_start = $offset; + $bytes_end = $offset + $length - 1; + + header('HTTP/1.1 206 Partial Content'); + // A full-length file will indeed be "bytes 0-x/x+1", think of 0-indexed array counts + header('Content-Range: bytes ' . $bytes_start . '-' . $bytes_end . '/' . $filesize); + // While playing media by direct link (not via FM) FireFox and IE doesn't allow seeking (rewind) it in player + // This header can fix this behavior if to put it out of this condition, but it breaks PDF preview + header('Accept-Ranges: bytes'); + } + + header('Content-Type: ' . mime_content_type($current_path)); header("Content-Transfer-Encoding: binary"); - header("Content-length: " . filesize($current_path)); + header("Content-Length: " . $length); header('Content-Disposition: inline; filename="' . basename($current_path) . '"'); - readfile($current_path); - exit(); + $fp = fopen($current_path, 'r'); + fseek($fp, $offset); + $position = 0; + + while($position < $length) { + $chunk = min($length - $position, 1024 * 8); + + echo fread($fp, $chunk); + flush(); + ob_flush(); + + $position += $chunk; + } + exit; } /** @@ -559,7 +596,7 @@ public function getimage($thumbnail) { $current_path = $this->getFullPath($this->get['path'], true); - $this->__log('loading image "' . $current_path . '"'); + Log::info('loading image "' . $current_path . '"'); // if $thumbnail is set to true we return the thumbnail if($thumbnail === true && $this->config['images']['thumbnail']['enabled'] === true) { @@ -586,7 +623,7 @@ public function delete() $current_path = $this->getFullPath($this->get['path'], true); $thumbnail_path = $this->get_thumbnail_path($current_path); - $this->__log('deleting "' . $current_path . '"'); + Log::info('deleting "' . $current_path . '"'); if(!$this->has_permission('delete')) { $this->error(sprintf($this->lang('NOT_ALLOWED'))); @@ -604,7 +641,7 @@ public function delete() if(is_dir($current_path)) { $this->unlinkRecursive($current_path); - $this->__log('deleted "' . $current_path . '"'); + Log::info('deleted "' . $current_path . '"'); // delete thumbnails if exists if(file_exists($thumbnail_path)) { @@ -612,7 +649,7 @@ public function delete() } } else { unlink($current_path); - $this->__log('deleted "' . $current_path . '"'); + Log::info('deleted "' . $current_path . '"'); // delete thumbnails if exists if(file_exists($thumbnail_path)) { @@ -635,7 +672,7 @@ public function download($force) $current_path = $this->getFullPath($this->get['path'], true); $filename = basename($current_path); - $this->__log('file downloading "' . $current_path . '"'); + Log::info('file downloading "' . $current_path . '"'); if(!$this->has_permission('download')) { $this->error(sprintf($this->lang('NOT_ALLOWED'))); @@ -692,7 +729,7 @@ public function download($force) header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); readfile($current_path); - $this->__log('file downloaded "' . $current_path . '"'); + Log::info('file downloaded "' . $current_path . '"'); exit(); } @@ -805,13 +842,13 @@ protected function has_system_permission($filepath, $permissions) { if(in_array('r', $permissions)) { if(!is_readable($filepath)) { - $this->__log('Not readable path "' . $filepath . '"'); + Log::info('Not readable path "' . $filepath . '"'); return false; }; } if(in_array('w', $permissions)) { if(!is_writable($filepath)) { - $this->__log('Not writable path "' . $filepath . '"'); + Log::info('Not writable path "' . $filepath . '"'); return false; } } @@ -826,59 +863,29 @@ protected function has_system_permission($filepath, $permissions) protected function get_file_info($relative_path) { $current_path = $this->getFullPath($relative_path); - $dynamic_path = $this->getDynamicPath($current_path); $item = $this->defaultInfo; $pathInfo = pathinfo($current_path); $filemtime = filemtime($current_path); - $iconsFolder = $this->getFmUrl($this->config['icons']['path']); - - // tell if we serve the files directly or if we should pass through the connector - $beyondDocRoot = stripos(realpath($this->path_to_files), realpath($this->doc_root)) !== 0; - $getImageMode = $this->connector_script_url . '?mode=getimage&path=' . rawurlencode($relative_path) . '&time=' . time(); - $readFileMode = $this->connector_script_url . '?mode=readfile&path=' . rawurlencode($relative_path) . '&time=' . time(); // check if file is writable and readable $protected = $this->has_system_permission($current_path, array('w', 'r')) ? 0 : 1; if(is_dir($current_path)) { $fileType = self::FILE_TYPE_DIR; - $thumbPath = $iconsFolder . ($protected ? 'locked_' : '') . $this->config['icons']['directory']; } else { $fileType = $pathInfo['extension']; - if($protected == 1) { - $thumbPath = $iconsFolder . 'locked_' . $this->config['icons']['default']; - } else { - $item['Properties']['Size'] = $this->get_real_filesize($current_path); - $thumbPath = $iconsFolder . $this->config['icons']['default']; - - $showThumbs = $this->config['options']['showThumbs']; - $isAllowedImage = in_array(strtolower($fileType), array_map('strtolower', $this->config['images']['imagesExt'])); - $isImageWithThumb = $isAllowedImage && $showThumbs; + $item['Properties']['Size'] = $this->get_real_filesize($current_path); - if(!$isImageWithThumb && file_exists($this->fm_path . '/' . $this->config['icons']['path'] . strtolower($fileType) . '.png')) { - $thumbPath = $iconsFolder . strtolower($fileType) . '.png'; + if(in_array(strtolower($fileType), array_map('strtolower', $this->config['images']['imagesExt']))) { + if($item['Properties']['Size']) { + list($width, $height, $type, $attr) = getimagesize($current_path); + } else { + list($width, $height) = array(0, 0); } - if($isAllowedImage) { - if($showThumbs) { - // svg don't need to generate a thumb - if($fileType === 'svg') { - $thumbPath = $beyondDocRoot ? $readFileMode : $dynamic_path; - } else { - $thumbPath = $beyondDocRoot ? $getImageMode . '&thumbnail=true' : $this->getDynamicPath($this->get_thumbnail($current_path)); - } - } - - if($item['Properties']['Size']) { - list($width, $height, $type, $attr) = getimagesize($current_path); - } else { - list($width, $height) = array(0, 0); - } - - $item['Properties']['Height'] = $height; - $item['Properties']['Width'] = $width; - } + $item['Properties']['Height'] = $height; + $item['Properties']['Width'] = $width; } } @@ -886,8 +893,6 @@ protected function get_file_info($relative_path) $item['Filename'] = $pathInfo['basename']; $item['File Type'] = $fileType; $item['Protected'] = $protected; - $item['Thumbnail'] = $thumbPath; - $item['Preview'] = $beyondDocRoot ? $readFileMode : $dynamic_path; $item['Properties']['Date Modified'] = $this->formatDate($filemtime); //$item['Properties']['Date Created'] = $this->formatDate(filectime($current_path)); // PHP cannot get create timestamp $item['Properties']['filemtime'] = $filemtime; @@ -961,9 +966,9 @@ protected function is_valid_path($path) $match = ($rp_substr === $rp_files); if(!$match) { - $this->__log('Invalid path "' . $path . '"'); - $this->__log('real path: "' . $rp_substr . '"'); - $this->__log('path to files: "' . $rp_files . '"'); + Log::info('Invalid path "' . $path . '"'); + Log::info('real path: "' . $rp_substr . '"'); + Log::info('path to files: "' . $rp_files . '"'); } return $match; } @@ -1269,7 +1274,7 @@ protected function get_thumbnail($path) protected function createThumbnail($imagePath, $thumbnailPath) { if($this->config['images']['thumbnail']['enabled'] === true) { - $this->__log('generating thumbnail "' . $thumbnailPath . '"'); + Log::info('generating thumbnail "' . $thumbnailPath . '"'); // create folder if it does not exist if(!file_exists(dirname($thumbnailPath))) { diff --git a/connectors/php/application/Fm.php b/connectors/php/application/Fm.php new file mode 100644 index 00000000..848fb75f --- /dev/null +++ b/connectors/php/application/Fm.php @@ -0,0 +1,59 @@ +logger = new Logger(); + } + + public function getInstance() + { + $serverConfig = require_once(FM_ROOT_PATH . '/config.php'); + + if (isset($serverConfig['logger']) && $serverConfig['logger']['enabled'] == true ) { + $this->logger->enabled = true; + } + + if (isset($serverConfig['plugin']) && !empty($serverConfig['plugin'])) { + $pluginName = $serverConfig['plugin']; + $pluginPath = FM_ROOT_PATH . "/plugins/{$pluginName}/"; + $className = ucfirst($pluginName) . 'Filemanager'; + require_once($pluginPath . $className . '.php'); + $pluginConfig = require_once($pluginPath . 'config.php'); + $config = array_replace_recursive($serverConfig, $pluginConfig); + $fm = new $className($config); + } else { + require_once(FM_ROOT_PATH . '/LocalFilemanager.php'); + $fm = new LocalFilemanager($serverConfig); + } + + if(!auth()) { + $fm->error($fm->lang('AUTHORIZATION_REQUIRED')); + } + + return $fm; + } + + /** + * Invokes filemanager action based on request params and returns response + * @param $fm BaseFilemanager + */ + public function handleRequest($fm) + { + $response = ''; + + if(!isset($_GET)) { + $this->error($this->lang('INVALID_ACTION')); + } else { + + if(isset($_GET['mode']) && $_GET['mode']!='') { + + switch($_GET['mode']) { + + default: + $this->error($this->lang('MODE_ERROR')); + break; + + case 'getinfo': + if($this->getvar('path')) { + $response = $this->getinfo(); + } + break; + + case 'getfolder': + if($this->getvar('path')) { + $response = $this->getfolder(); + } + break; + + case 'rename': + if($this->getvar('old') && $this->getvar('new')) { + $response = $this->rename(); + } + break; + + case 'move': + if($this->getvar('old') && $this->getvar('new')) { + $response = $this->move(); + } + break; + + case 'editfile': + if($this->getvar('path')) { + $response = $this->editfile(); + } + break; + + case 'delete': + if($this->getvar('path')) { + $response = $this->delete(); + } + break; + + case 'addfolder': + if($this->getvar('path') && $this->getvar('name')) { + $response = $this->addfolder(); + } + break; + + case 'download': + if($this->getvar('path')) { + $force = isset($_GET['force']); + $response = $this->download($force); + } + break; + + case 'getimage': + if($this->getvar('path')) { + $thumbnail = isset($_GET['thumbnail']); + $this->getimage($thumbnail); + } + break; + + case 'readfile': + if($this->getvar('path')) { + $this->readfile(); + } + break; + + case 'summarize': + $response = $this->summarize(); + break; + } + + } else if(isset($_POST['mode']) && $_POST['mode']!='') { + + switch($_POST['mode']) { + + default: + $this->error($this->lang('MODE_ERROR')); + break; + + case 'add': + if($this->postvar('currentpath')) { + $this->add(); + } + break; + + case 'replace': + if($this->postvar('newfilepath')) { + $this->replace(); + } + break; + + case 'savefile': + if($this->postvar('content', false) && $this->postvar('path')) { + $response = $this->savefile(); + } + break; + } + } + } + + echo json_encode($response); + die(); + } + + /** + * Retrieve data from $_GET global var + * @param string $var + * @param bool $sanitize + * @return bool + */ + public function getvar($var, $sanitize = true) + { + if(!isset($_GET[$var]) || $_GET[$var]=='') { + $this->error(sprintf($this->lang('INVALID_VAR'),$var)); + } else { + if($sanitize) { + $this->get[$var] = $this->sanitize($_GET[$var]); + } else { + $this->get[$var] = $_GET[$var]; + } + return true; + } + } + + public function get($name = null, $defaultValue = null) + { + return isset($params[$name]) ? $params[$name] : $defaultValue; + + + if(!isset($_GET[$var]) || $_GET[$var]=='') { + $this->error(sprintf($this->lang('INVALID_VAR'),$var)); + } else { + if($sanitize) { + $this->get[$var] = $this->sanitize($_GET[$var]); + } else { + $this->get[$var] = $_GET[$var]; + } + return true; + } + } + + /** + * Retrieve data from $_POST global var + * @param string $var + * @param bool $sanitize + * @return bool + */ + public function postvar($var, $sanitize = true) + { + if(!isset($_POST[$var]) || ($var != 'content' && $_POST[$var]=='')) { + $this->error(sprintf($this->lang('INVALID_VAR'),$var)); + } else { + if($sanitize) { + $this->post[$var] = $this->sanitize($_POST[$var]); + } else { + $this->post[$var] = $_POST[$var]; + } + return true; + } + } + + /** + * Retrieve data from $_SERVER global var + * @param string $var + * @param string|null $default + * @return bool + */ + public function get_server_var($var, $default = null) + { + return !isset($_SERVER[$var]) ? $default : $_SERVER[$var]; + } + + /** + * Sanitize global vars: $_GET, $_POST + * @param string $var + * @return mixed|string + */ + protected function sanitize($var) + { + $sanitized = strip_tags($var); + $sanitized = str_replace('http://', '', $sanitized); + $sanitized = str_replace('https://', '', $sanitized); + $sanitized = str_replace('../', '', $sanitized); + + return $sanitized; + } +} \ No newline at end of file diff --git a/connectors/php/application/Logger.php b/connectors/php/application/Logger.php new file mode 100644 index 00000000..7289fae1 --- /dev/null +++ b/connectors/php/application/Logger.php @@ -0,0 +1,99 @@ +file = sys_get_temp_dir() . '/filemanager.log'; + $this->file ='C:/filemanager.log'; + } + + /** + * Log message + * @param string $message + */ + public function log($message) + { + if ($this->enabled) { + $entry = $this->formatMessage($message); + $fp = fopen($this->file, "a"); + fwrite($fp, $entry . PHP_EOL); + fclose($fp); + } + } + + /** + * Formats a log message for display as a string. + * @param string $message + * @return string + */ + protected function formatMessage($message) + { + $traces = array(); + foreach ($this->getBacktrace() as $trace) { + $traces[] = "in {$trace['file']}:{$trace['line']}"; + } + + $str = "[" . date('Y-m-d H:i:s', time()) . "]#" . $this->getUserIp() . "# - " . $message; + $str .= (empty($traces) ? '' : "\n " . implode("\n ", $traces)); + return $str; + } + + /** + * Returns backtrace stack according to $traceLevel + * @return array + */ + protected function getBacktrace() + { + $traces = array(); + if ($this->traceLevel > 0) { + $count = 0; + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + array_pop($backtrace); // remove the last trace since it would be the entry script, not very useful + foreach ($backtrace as $trace) { + if (isset($trace['file'], $trace['line']) && strpos($trace['file'], FM_APP_PATH) !== 0) { + unset($trace['object'], $trace['args']); + $traces[] = $trace; + if (++$count >= $this->traceLevel) { + break; + } + } + } + } + return $traces; + } + + /** + * Return user IP address + * @return mixed + */ + protected function getUserIp() + { + $client = @$_SERVER['HTTP_CLIENT_IP']; + $forward = @$_SERVER['HTTP_X_FORWARDED_FOR']; + $remote = $_SERVER['REMOTE_ADDR']; + + if (filter_var($client, FILTER_VALIDATE_IP)) { + $ip = $client; + } elseif (filter_var($forward, FILTER_VALIDATE_IP)) { + $ip = $forward; + } else { + $ip = $remote; + } + + return $ip; + } +} \ No newline at end of file diff --git a/connectors/php/application/facade/Log.php b/connectors/php/application/facade/Log.php new file mode 100644 index 00000000..4b40580e --- /dev/null +++ b/connectors/php/application/facade/Log.php @@ -0,0 +1,14 @@ +logger->log($message); + } +} \ No newline at end of file diff --git a/connectors/php/config.php b/connectors/php/config.php index c720a20f..55f177f1 100644 --- a/connectors/php/config.php +++ b/connectors/php/config.php @@ -39,4 +39,8 @@ //$config['plugin'] = 's3'; $config['plugin'] = null; +$config['logger'] = array( + 'enabled' => true, +); + return $config; \ No newline at end of file diff --git a/connectors/php/filemanager.php b/connectors/php/filemanager.php index a611dea2..c2118059 100755 --- a/connectors/php/filemanager.php +++ b/connectors/php/filemanager.php @@ -15,6 +15,7 @@ // ini_set('display_errors', '1'); require_once('BaseHelper.php'); +require_once('application/Fm.php'); function auth() { @@ -24,7 +25,7 @@ function auth() return true; } -$fm = BaseHelper::getInstance(); +$fm = Fm::app()->getInstance(); // use to setup files root folder //$fm->setFileRoot('userfiles', true); diff --git a/connectors/php/plugins/s3/S3Filemanager.php b/connectors/php/plugins/s3/S3Filemanager.php index ac9c6623..448d3627 100644 --- a/connectors/php/plugins/s3/S3Filemanager.php +++ b/connectors/php/plugins/s3/S3Filemanager.php @@ -224,7 +224,7 @@ public function add() } // end application to prevent double response (uploader and filemanager) - die(); + exit; } /** @@ -251,7 +251,7 @@ public function addfolder() 'Error' => "", 'Code' => 0, ); - $this->__log(__METHOD__ . ' - adding folder ' . $new_dir); + Log::info(__METHOD__ . ' - adding folder ' . $new_dir); return $array; } @@ -520,7 +520,7 @@ public function editfile() $this->error(sprintf($this->lang('NOT_ALLOWED'))); } - $this->__log(__METHOD__ . ' - editing file '. $current_path); + Log::info(__METHOD__ . ' - editing file '. $current_path); $content = file_get_contents($current_path); $content = htmlspecialchars($content); @@ -550,7 +550,7 @@ public function savefile() $this->error(sprintf($this->lang('NOT_ALLOWED'))); } - $this->__log(__METHOD__ . ' - saving file '. $current_path); + Log::info(__METHOD__ . ' - saving file '. $current_path); $content = htmlspecialchars_decode($this->post['content']); $r = file_put_contents($current_path, $content); @@ -569,22 +569,61 @@ public function savefile() } /** + * Seekable stream: http://stackoverflow.com/a/23046071/1789808 * @inheritdoc - * TODO: audio and video files at S3 are not seekable with html5 player, also they hang filemanager, - * TODO: until it's fully loaded from S3. Perhaps that may be solved via streaming from CloudFront - * @see http://stackoverflow.com/questions/13115618/player-for-video-streaming-from-amazon-s3-cloudfront */ public function readfile() { $current_path = $this->getFullPath($this->get['path'], true); + $filesize = filesize($current_path); + $length = $filesize; + $context = null; + + if(isset($_SERVER['HTTP_RANGE'])) { + if(!preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches)) { + header('HTTP/1.1 416 Requested Range Not Satisfiable'); + header('Content-Range: bytes */' . $filesize); + exit; + } + + $offset = intval($matches[1]); + + if(isset($matches[2])) { + $end = intval($matches[2]); + if($offset > $end) { + header('HTTP/1.1 416 Requested Range Not Satisfiable'); + header('Content-Range: bytes */' . $filesize); + exit; + } + $length = $end - $offset; + } else { + $length = $filesize - $offset; + } - header('Content-type: ' . $this->getMimeType($current_path)); + $bytes_start = $offset; + $bytes_end = $offset + $length - 1; + $context = stream_context_create(array( + 's3' => array( + 'seekable' => true, + 'Range' => "bytes={$bytes_start}-{$bytes_end}", + ) + )); + + header('HTTP/1.1 206 Partial Content'); + // A full-length file will indeed be "bytes 0-x/x+1", think of 0-indexed array counts + header('Content-Range: bytes ' . $bytes_start . '-' . $bytes_end . '/' . $filesize); + // While playing media by direct link (not via FM) FireFox and IE doesn't allow seeking (rewind) it in player + // This header can fix this behavior if to put it out of this condition, but it breaks PDF preview + header('Accept-Ranges: bytes'); + } + + header('Content-Type: ' . $this->getMimeType($current_path)); header("Content-Transfer-Encoding: binary"); - header("Content-length: " . filesize($current_path)); + header("Content-Length: " . $length); header('Content-Disposition: inline; filename="' . basename($current_path) . '"'); - readfile($current_path); - exit(); + readfile($current_path, null, $context); + exit; } /** @@ -684,9 +723,9 @@ public function download($force) header('Expires: 0'); header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); - $this->__log(__METHOD__ . ' - downloading '. $current_path); + Log::info(__METHOD__ . ' - downloading '. $current_path); readfile($current_path); - $this->__log(__METHOD__ . ' - downloaded '. $current_path); + Log::info(__METHOD__ . ' - downloaded '. $current_path); exit(); } @@ -801,73 +840,24 @@ protected function get_file_info($relative_path, $thumbnail = false) $item = $this->defaultInfo; $pathInfo = pathinfo($current_path); $filemtime = @filemtime($current_path); - $iconsFolder = $this->getFmUrl($this->config['icons']['path']); - - // check comment in "getinfo()" method - $protected = false; if(is_dir($current_path)) { $fileType = self::FILE_TYPE_DIR; - $thumbPath = $iconsFolder . ($protected ? 'locked_' : '') . $this->config['icons']['directory']; } else { $fileType = $pathInfo['extension']; - if($protected) { - $thumbPath = $iconsFolder . 'locked_' . $this->config['icons']['default']; - } else { - $thumbPath = $iconsFolder . $this->config['icons']['default']; - $item['Properties']['Size'] = filesize($current_path); - - if($this->config['options']['showThumbs'] && in_array(strtolower($fileType), array_map('strtolower', $this->config['images']['imagesExt']))) { - // svg should not be previewed as raster formats images - $is_svg = ($fileType === 'svg'); - - if($this->config['s3']['thumbsRetrieveMode'] === self::RETRIEVE_MODE_BROWSER || $is_svg) { - $thumbPath = $this->getS3Url($current_path, $thumbnail && !$is_svg); - } else { - $thumbPath = $this->connector_script_url . '?mode=getimage&path=' . rawurlencode($relative_path) . '&time=' . time(); - if($thumbnail) $thumbPath .= '&thumbnail=true'; - } - } else if(file_exists($this->fm_path . '/' . $this->config['icons']['path'] . strtolower($fileType) . '.png')) { - $thumbPath = $iconsFolder . strtolower($fileType) . '.png'; - } - } + $item['Properties']['Size'] = filesize($current_path); } $item['Path'] = $this->getDynamicPath($current_path); $item['Filename'] = $pathInfo['basename']; $item['File Type'] = $fileType; - $item['Protected'] = (int)$protected; - $item['Thumbnail'] = $thumbPath; - // for preview mode only - if($thumbnail === false) { - $item['Preview'] = $this->connector_script_url . '?mode=readfile&path=' . $relative_path; - } - + $item['Protected'] = 0; // check comment in "getinfo()" method $item['Properties']['Date Modified'] = $this->formatDate($filemtime); //$item['Properties']['Date Created'] = $this->formatDate(filectime($current_path)); // PHP cannot get create timestamp $item['Properties']['filemtime'] = $filemtime; return $item; } - /** - * Creates url to S3 object - * @param string $filePath - * @param boolean $thumbnail - * @return mixed - */ - protected function getS3Url($filePath, $thumbnail = false) - { - $path = $thumbnail ? $this->get_thumbnail($filePath) : $filePath; - - if($this->config['s3']['presignUrl']) { - return $this->s3->getPresignedUrl($path, '+10 minutes'); - } else { - // TODO: is non-presigned url might be created in place, without extra request ? - //sprintf('//%s/%s', $this->domain, $this->bucket.'/'.$path) - return $this->s3->getUrl($path); - } - } - /** * Checks path for "dots" to avoid directory climbing and backtracking (traversal attack) * Probably there is an ability to disable such "relative path" via S3 settings, research required @@ -1089,7 +1079,7 @@ protected function get_thumbnail($path) protected function createThumbnail($imagePath, $thumbnailPath) { if($this->config['images']['thumbnail']['enabled'] === true) { - $this->__log(__METHOD__ . ' - generating thumbnail: '. $thumbnailPath); + Log::info(__METHOD__ . ' - generating thumbnail: '. $thumbnailPath); $this->initUploader(array( 'upload_dir' => dirname($imagePath) . '/', diff --git a/images/fileicons/odp.png b/images/fileicons/odp.png new file mode 100644 index 0000000000000000000000000000000000000000..2b3a5bb90065c167ca7bd9d33fbf799350cfe1b7 GIT binary patch literal 13654 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 L&Fi5DCFB1949ciD literal 0 HcmV?d00001 diff --git a/images/fileicons/ods.png b/images/fileicons/ods.png new file mode 100644 index 0000000000000000000000000000000000000000..ca480522d847e3f4d706ffee107d2225c75b2377 GIT binary patch literal 12338 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 literal 0 HcmV?d00001 diff --git a/scripts/filemanager.config.default.json b/scripts/filemanager.config.default.json index 6e6ae384..f376068d 100644 --- a/scripts/filemanager.config.default.json +++ b/scripts/filemanager.config.default.json @@ -178,7 +178,8 @@ }, "icons": { "path": "images/fileicons/", - "directory": "_Open.png", + "folder": "_Open.png", + "parent": "_Parent.png", "default": "default.png" }, "url": "https://github.com/servocoder/RichFilemanager", diff --git a/scripts/filemanager.js b/scripts/filemanager.js index 94e4007d..1329b592 100644 --- a/scripts/filemanager.js +++ b/scripts/filemanager.js @@ -52,7 +52,7 @@ var normalizePath = function(path){ // Retrieves config settings from filemanager.config.json var loadConfigFile = function (type) { var json = null, - pluginPath = "."; + pluginPath = '.'; type = (typeof type === "undefined") ? "user" : type; if (window._FMConfig && window._FMConfig.pluginPath) { @@ -108,9 +108,8 @@ var fileRoot = '/'; // Base URL to access the filemanager var baseUrl; -// Sets paths to connectors based on language selection. -var langConnector = '/connectors/' + config.options.lang + '/filemanager.' + config.options.lang; -var fileConnector = config.options.fileConnector || config.globals.pluginPath + langConnector; +// URL to API connector, based on `baseUrl` if not specified explicitly +var fileConnector; // Read capabilities from config files if exists else apply default settings var capabilities = config.options.capabilities || ['upload', 'select', 'download', 'rename', 'move', 'delete', 'replace']; @@ -413,6 +412,14 @@ var trimSlashes = function(string) { return string.replace(/^\/+|\/+$/g, ''); }; +var encodePath = function(path) { + var parts = []; + $.each(path.split('/'), function(i, part) { + parts.push(encodeURIComponent(part)); + }); + return parts.join('/'); +}; + // from http://phpjs.org/functions/basename:360 var basename = function(path, suffix) { var b = path.replace(/^.*[\/\\]/g, ''); @@ -519,28 +526,60 @@ var isDocumentFile = function(filename) { } }; +var buildConnectorUrl = function(params) { + var defaults = { + config: userconfig, + time: new Date().getTime() + }; + var queryParams = $.extend({}, params || {}, defaults); + return fileConnector + '?' + $.param(queryParams); +}; + // Build url to preview files -var createPreviewUrl = function(path, encode) { - encode = encode || false; - // already an absolute path or a relative path to connector action - if(path.substr(0,4) === 'http' || path.substr(0,3) === 'ftp' || path.indexOf(langConnector) !== -1) { - return path; - } - path = trimSlashes(normalizePath(path)); +var createPreviewUrl = function(path) { + return buildConnectorUrl({ + mode: 'readfile', + path: path + }); +}; - if(encode) { - var parts = []; - $.each(path.split('/'), function(i, part) { - parts.push(encodeURIComponent(part)); - }); - path = parts.join('/'); +var createImageUrl = function(data, thumbnail) { + var imagePath; + if(!isFile(data['Path'])) { + imagePath = baseUrl + config.icons.path + (data['Protected'] == 1 ? 'locked_' : '') + config.icons.folder; + } else { + if(data['Protected'] == 1) { + imagePath = baseUrl + config.icons.path + 'locked_' + config.icons.folder; + } else { + var fileType = getExtension(data['Path']); + var isAllowedImage = isImageFile(data['Path']); + var fileTypeIcon = baseUrl + config.icons.path + fileType + '.png'; + imagePath = baseUrl + config.icons.path + config.icons.default; + + if(!(isAllowedImage && config.options.showThumbs) && file_exists(fileTypeIcon)) { + imagePath = fileTypeIcon; + } + if(isAllowedImage) { + var queryParams = {path: data['Path']}; + if(fileType === 'svg') { + queryParams.mode = 'readfile'; + } else { + queryParams.mode = 'getimage'; + if(thumbnail) { + queryParams.thumbnail = 'true'; + } + } + imagePath = buildConnectorUrl(queryParams); + } + } } - return baseUrl + path; + console.log('imagePath', imagePath); + return imagePath; }; // Return HTML video player var getVideoPlayer = function(data) { - var url = createPreviewUrl(data['Preview'], true); + var url = createPreviewUrl(data['Path']); var code = ''; $fileinfo.find('img').remove(); @@ -549,7 +588,7 @@ var getVideoPlayer = function(data) { // Return HTML audio player var getAudioPlayer = function(data) { - var url = createPreviewUrl(data['Preview'], true); + var url = createPreviewUrl(data['Path']); var code = ''; $fileinfo.find('img').remove(); @@ -558,7 +597,7 @@ var getAudioPlayer = function(data) { // Return PDF Reader var getPdfReader = function(data) { - var url = createPreviewUrl(data['Preview'], true); + var url = createPreviewUrl(data['Path']); var code = ''; $fileinfo.find('img').remove(); @@ -567,7 +606,7 @@ var getPdfReader = function(data) { // Return Google Viewer var getGoogleViewer = function(data) { - var url = encodeURIComponent(createPreviewUrl(data['Preview'])); + var url = encodeURIComponent(createPreviewUrl(data['Path'])); var code = ''; $fileinfo.find('img').remove(); @@ -604,7 +643,11 @@ var setUploader = function(path) { if(fname != '') { foldername = cleanString(fname); - $.getJSON(fileConnector + '?mode=addfolder&path=' + getCurrentPath() + '&config=' + userconfig + '&name=' + encodeURIComponent(foldername) + '&time=' + new Date().getTime(), function(result) { + $.getJSON(buildConnectorUrl({ + mode: 'addfolder', + path: getCurrentPath(), + name: foldername + }), function(result) { if(result['Code'] == 0) { addFolder(result['Parent']); getFolderInfo(result['Parent']); @@ -876,7 +919,7 @@ var createFileTree = function() { // contextual menu option in list views. // NOTE: closes the window when finished. var selectItem = function(data) { - var url = createPreviewUrl(data['Preview']); + var url = createPreviewUrl(data['Path']); if(window.opener || window.tinyMCEPopup || $.urlParam('field_name') || $.urlParam('CKEditorCleanUpFuncNum') || $.urlParam('CKEditor') || $.urlParam('ImperaviElementId')) { if(window.tinyMCEPopup) { // use TinyMCE > 3.0 integration method @@ -993,11 +1036,13 @@ var renameItem = function(data) { return false; } - var connectString = fileConnector + '?mode=rename&old=' + encodeURIComponent(data['Path']) + '&new=' + encodeURIComponent(givenName) + '&config=' + userconfig; - $.ajax({ type: 'GET', - url: connectString, + url: buildConnectorUrl({ + mode: 'rename', + old: data['Path'], + new: givenName + }), dataType: 'json', async: false, success: function(result) { @@ -1075,7 +1120,7 @@ var replaceItem = function(itemData) { .fileupload({ autoUpload: true, dataType: 'json', - url: fileConnector + '?config=' + userconfig, + url: buildConnectorUrl(), paramName: config.upload.paramName }) @@ -1172,11 +1217,13 @@ var moveItemPrompt = function(data) { // Called by clicking the "Move" button in detail views // or choosing the "Move" contextual menu option in list views. var moveItem = function(oldPath, newPath) { - var connectString = fileConnector + '?mode=move&old=' + encodeURIComponent(oldPath) + '&new=' + encodeURIComponent(newPath) + '&config=' + userconfig; - $.ajax({ type: 'GET', - url: connectString, + url: buildConnectorUrl({ + mode: 'move', + old: oldPath, + new: newPath + }), dataType: 'json', async: false, success: function(result) { @@ -1231,11 +1278,13 @@ var deleteItem = function(data) { var doDelete = function(e, value, message, formVals) { if(!value) return; - var connectString = fileConnector + '?mode=delete&path=' + encodeURIComponent(data['Path']) + '&config=' + userconfig + '&time=' + new Date().getTime(); $.ajax({ type: 'GET', - url: connectString, + url: buildConnectorUrl({ + mode: 'delete', + path: data['Path'] + }), dataType: 'json', async: false, success: function(result) { @@ -1283,16 +1332,20 @@ var deleteItem = function(data) { // Called by clicking the "Download" button in detail views // or choosing the "Download" contextual menu item in list views. var downloadItem = function(data) { - var connectString = fileConnector + '?mode=download&path=' + encodeURIComponent(data['Path']) + '&config=' + userconfig; + var queryParams = { + mode: 'download', + path: data['Path'] + }; $.ajax({ type: 'GET', - url: connectString + '&time=' + new Date().getTime(), + url: buildConnectorUrl(queryParams), dataType: 'json', async: false, success: function(result) { if(result['Code'] == 0) { - window.location = connectString + '&force=true&time=' + new Date().getTime(); + queryParams.force = 'true'; + window.location = buildConnectorUrl(queryParams); } else { $.prompt(result['Error']); } @@ -1310,11 +1363,13 @@ var editItem = function(data) { $('#edit-file').click(function() { $(this).hide(); // hiding Edit link - var connectString = fileConnector + '?mode=editfile&path=' + encodeURIComponent(data['Path']) + '&config=' + userconfig + '&time=' + new Date().getTime(); $.ajax({ type: 'GET', - url: connectString, + url: buildConnectorUrl({ + mode: 'editfile', + path: data['Path'] + }), dataType: 'json', async: false, success: function (result) { @@ -1352,7 +1407,7 @@ var editItem = function(data) { $.ajax({ type: 'POST', - url: fileConnector + '?config=' + userconfig, + url: buildConnectorUrl(), dataType: 'json', data: postData, async: false, @@ -1781,7 +1836,10 @@ function getContextMenuItems($item) { // Binds contextual menus to items in list and grid views. var setMenus = function(action, path) { - $.getJSON(fileConnector + '?mode=getinfo&path=' + encodeURIComponent(path) + '&config=' + userconfig + '&time=' + new Date().getTime(), function(data) { + $.getJSON(buildConnectorUrl({ + mode: 'getinfo', + path: path + }), function(data) { switch(action) { case 'select': selectItem(data); @@ -1826,7 +1884,10 @@ var getFileInfo = function(file) { setUploader(currentpath); // Retrieve the data & populate the template. - $.getJSON(fileConnector + '?mode=getinfo&path=' + encodeURIComponent(file) + '&config=' + userconfig + '&time=' + new Date().getTime(), function(data) { + $.getJSON(buildConnectorUrl({ + mode: 'getinfo', + path: file + }), function(data) { // is there any error or user is unauthorized if(data.Code == '-1') { handleError(data.Error); @@ -1851,9 +1912,7 @@ var getFileInfo = function(file) { // add the new markup to the DOM getSectionContainer($fileinfo).html(template); - // if file is an image we display the preview - var previewPath = isImageFile(data['Filename']) ? data['Preview'] : data['Thumbnail']; - $fileinfo.find('img').attr('src', createPreviewUrl(previewPath)); + $fileinfo.find('img').attr('src', createImageUrl(data)); $fileinfo.find('#main-title > h1').text(data['Filename']).attr('title', file); if(isVideoFile(data['Filename']) && config.videos.showVideoPlayer == true) { @@ -1868,13 +1927,12 @@ var getFileInfo = function(file) { if(isDocumentFile(data['Filename']) && config.docs.showGoogleViewer == true) { getGoogleViewer(data); } - if(isEditableFile(data['Filename']) && config.edit.enabled == true && data['Protected']==0) { + if(isEditableFile(data['Filename']) && config.edit.enabled == true && data['Protected'] == 0) { editItem(data); } - var url = createPreviewUrl(data['Preview']); - if(data['Protected']==0) { - $fileinfo.find('div#tools').append(' ' + lg.copy_to_clipboard + ''); + if(data['Protected'] == 0) { + $fileinfo.find('div#tools').append('' + lg.copy_to_clipboard + ''); // zeroClipboard code ZeroClipboard.config({swfPath: config.globals.pluginPath + '/scripts/zeroclipboard/dist/ZeroClipboard.swf'}); @@ -1909,7 +1967,6 @@ var getFileInfo = function(file) { // Clean up unnecessary item data var prepareItemInfo = function(item) { var data = $.extend({}, item); - delete data['Thumbnail']; delete data['Error']; delete data['Code']; return data; @@ -1950,7 +2007,7 @@ var getFolderInfo = function(path) { if(!isFile(path) && path !== fileRoot) { parentNode = '
  • '; - parentNode += '
    Parent
    '; + parentNode += '
    Parent
    '; parentNode += '
  • '; $ul.append(parentNode); } @@ -1969,7 +2026,7 @@ var getFolderInfo = function(path) { 'data-path': item['Path'] }).data('itemdata', prepareItemInfo(item)); - node = '
    ' + item['Path'] + '
    '; + node = '
    ' + item['Path'] + '
    '; node += '

    ' + item['Filename'] + '

    '; if(props['Width'] && props['Width'] != '') node += '' + props['Width'] + 'x' + props['Height'] + ''; if(props['Size'] && props['Size'] != '') node += '' + props['Size'] + ''; @@ -2164,12 +2221,19 @@ var getFolderData = function(path) { // TODO: it is also possible to cache based on "source" (filetree / main window list) // caches result for specified path to get rid of redundant requests if(!loadedFolderData[path] || (Date.now() - loadedFolderData[path].cached) > 2000) { - var url = fileConnector + '?mode=getfolder&path=' + encodeURIComponent(path) + '&config=' + userconfig + '&showThumbs=' + config.options.showThumbs + '&time=' + new Date().getTime(); - if ($.urlParam('type')) url += '&type=' + $.urlParam('type'); + var queryParams = { + mode: 'getfolder', + path: path, + showThumbs: config.options.showThumbs + }; + + if($.urlParam('type')) { + queryParams.type = $.urlParam('type'); + } $.ajax({ 'async': false, - 'url': url, + 'url': buildConnectorUrl(queryParams), 'dataType': "json", 'cache': false, 'success': function(data) { @@ -2284,7 +2348,7 @@ $(function() { loadJS('/scripts/CodeMirror/dynamic-mode.js'); } - // init baseUrl + // set baseUrl if(config.options.baseUrl === false) { baseUrl = location.origin + location.pathname; } else { @@ -2296,6 +2360,10 @@ $(function() { } baseUrl = trimSlashes(baseUrl) + '/'; + // set fileConnector + var langConnector = 'connectors/' + config.options.lang + '/filemanager.' + config.options.lang; + fileConnector = config.options.fileConnector || baseUrl + langConnector; + // changes files root to restrict the view to a given folder if($.urlParam('exclusiveFolder') != 0) { fileRoot += $.urlParam('exclusiveFolder'); @@ -2383,7 +2451,9 @@ $(function() { var message = '
    ' + lg.summary_title + '
    '; var $prompt = $.prompt(message).addClass('summary-popup'); - $.getJSON(fileConnector + '?mode=summarize&config=' + userconfig, function(result) { + $.getJSON(buildConnectorUrl({ + mode: 'summarize' + }), function(result) { if(result['Code'] == 0) { var $content = $prompt.find('.jqimessage'), size = formatBytes(result['Size'], true); @@ -2499,11 +2569,11 @@ $(function() { file = data.files[0]; if(file.chunkUploaded) { - var path = currentPath + file.serverName, - url = fileConnector + '?mode=getinfo&path=' + encodeURIComponent(path) + '&config=' + userconfig + '&time=' + new Date().getTime(); - $.ajax({ - 'url': url, + 'url': buildConnectorUrl({ + mode: 'getinfo', + path: currentPath + file.serverName + }), 'dataType': "json", 'async': false, 'success': function(result) { @@ -2532,10 +2602,10 @@ $(function() { file = data.files[0]; if(file.chunkUploaded) { - var path = currentPath + file.serverName, - url = fileConnector + '?mode=delete&path=' + encodeURIComponent(path) + '&config=' + userconfig + '&time=' + new Date().getTime(); - - $.getJSON(url, function(result) { + $.getJSON(buildConnectorUrl({ + mode: 'delete', + path: currentPath + file.serverName + }), function(result) { if(result['Code'] == 0) { var path = result['Path']; removeNode(path); @@ -2576,7 +2646,7 @@ $(function() { dataType: 'json', dropZone: $dropzone, maxChunkSize: config.upload.chunkSize, - url: fileConnector + '?config=' + userconfig, + url: buildConnectorUrl(), paramName: config.upload.paramName, formData: { mode: 'add', @@ -2751,7 +2821,7 @@ $(function() { .fileupload({ autoUpload: false, dataType: 'json', - url: fileConnector + '?config=' + userconfig, + url: buildConnectorUrl(), paramName: config.upload.paramName }) diff --git a/scripts/filemanager.min.js b/scripts/filemanager.min.js index 5490a16a..ce2f8158 100644 --- a/scripts/filemanager.min.js +++ b/scripts/filemanager.min.js @@ -1,2 +1,2 @@ -!function(a){function b(b,c){return-1===m.indexOf(c)?!1:"dir"==b["File Type"]&&"replace"==c?!1:"dir"==b["File Type"]&&"select"==c?!1:"dir"==b["File Type"]&&"download"==c?g.security.allowFolderDownload===!0:"undefined"!=typeof b.Capabilities?a.inArray(c,b.Capabilities)>-1:!0}function c(c){var d={select:{name:B.select,className:"select"},download:{name:B.download,className:"download"},rename:{name:B.rename,className:"rename"},move:{name:B.move,className:"move"},replace:{name:B.replace,className:"replace"},separator1:"-----","delete":{name:B.del,className:"delete"}},e=c.data("itemdata");return b(e,"download")||delete d.download,b(e,"rename")&&g.options.browseOnly!==!0||delete d.rename,b(e,"delete")&&g.options.browseOnly!==!0||delete d["delete"],b(e,"move")&&g.options.browseOnly!==!0||delete d.move,b(e,"select")&&(window.opener||window.tinyMCEPopup||a.urlParam("field_name"))||delete d.select,delete d.replace,d}a.urlParam=function(a){var b=new RegExp("[\\?&]"+a+"=([^&#]*)").exec(window.location.href);return b?b[1]:0};var d=function(a){var b="",c="/",d=".",e=d.concat(d);if(!a||a===c)return c;var f,g,h,i=a.split(c),j=a[0]===c||a[0]===d?[b]:[];for(f=0,g=i.length;g>f;++f)h=i[f]||b,h===e?j.length>1&&j.pop():h!==b&&h!==d&&j.push(h);return j.join(c).replace(/[\/]{2,}/g,c)||c},e=function(b){var c=null,d=".";if(b="undefined"==typeof b?"user":b,window._FMConfig&&window._FMConfig.pluginPath&&(d=window._FMConfig.pluginPath),"user"==b)if(0!=a.urlParam("config")){var e=d+"/scripts/"+a.urlParam("config");userconfig=a.urlParam("config")}else{var e=d+"/scripts/filemanager.config.json";userconfig="filemanager.config.json"}else var e=d+"/scripts/filemanager.config.default.json";return a.ajax({async:!1,url:e,dataType:"json",cache:!1,success:function(a){c=a}}),"default"==b&&(c.globals={pluginPath:d}),c},f=e("default"),g=e();if(null!==g&&delete g.version,g=a.extend({},f,g),g.options.logger){(new Date).getTime()}var h,i=[],j="/",k="/connectors/"+g.options.lang+"/filemanager."+g.options.lang,l=g.options.fileConnector||g.globals.pluginPath+k,m=g.options.capabilities||["upload","select","download","rename","move","delete","replace"],n=null,o={},p=[];g.options.fileSorting&&(p=g.options.fileSorting.toLowerCase().split("_"));var q=p[0]||"name",r=p[1]||"asc",s=function(b){if(b=g.globals.pluginPath+b,-1==a.inArray(b,i)){var c=a("");a("head").append(c),i.push(b)}},t=function(b){if(b=g.globals.pluginPath+b,-1==a.inArray(b,i)){var c=a("