* @package main class */ class FileDownload { /** * modX object * @var object */ public $modx; /** * $scriptProperties * @var array */ public $config = array(); /** * To hold error message * @var string */ private $_error = ''; /** * To hold output message * @var string */ private $_output = array(); /** * To hold placeholder array, flatten array with prefixable * @var array */ private $_placeholders = array(); /** * To hold plugin * @var array */ public $plugins; /** * To hold counting * @var array */ private $_count = array(); /** * To hold image type * @var array */ private $_imgType = array(); /** * constructor * @param modX $modx * @param array $config parameters */ public function __construct(modX $modx, $config = array()) { $this->modx = &$modx; $config['getDir'] = !empty($config['getDir']) ? $this->_checkPath($config['getDir']) : ''; $config['origDir'] = !empty($config['getDir']) ? $config['getDir'] : ''; // getDir will be overridden by setDirProp() $config['getFile'] = !empty($config['getFile']) ? $this->_checkPath($config['getFile']) : ''; $config = $this->replacePropPhs($config); $corePath = $this->modx->getOption('core_path'); $basePath = $corePath . 'components/filedownloadr/'; $assetsUrl = $this->modx->getOption('assets_url') . 'components/filedownloadr/'; $this->_output = array( 'rows' => '', 'dirRows' => '', 'fileRows' => '' ); $this->config = array_merge(array( 'corePath' => $corePath, 'basePath' => $basePath, 'modelPath' => $basePath . 'models/', 'processorsPath' => $basePath . 'processors/', 'controllersPath' => $basePath . 'controllers/', 'chunksPath' => $basePath . 'elements/chunks/', 'jsUrl' => $assetsUrl . 'js/', 'cssUrl' => $assetsUrl . 'css/', 'imgTypeUrl' => $assetsUrl . 'img/filetypes/', 'imgLocat' => $assetsUrl . 'img/filetypes/', 'assetsUrl' => $assetsUrl, 'encoding' => 'utf-8' ), $config); $this->modx->addPackage('filedownload', $this->config['modelPath']); if (!$this->modx->lexicon) { $this->modx->getService('lexicon', 'modLexicon'); } $this->modx->lexicon->load('filedownloadr:default'); $this->_imgType = $this->_imgTypeProp(); if (!empty($this->config['encoding'])) mb_internal_encoding($this->config['encoding']); if (!empty($this->config['plugins'])) { if (!$this->modx->loadClass('filedownload.FileDownloadPlugin', $this->config['modelPath'], true, true)) { $this->modx->log(modX::LOG_LEVEL_ERROR, '[FileDownload] could not load plugin class.'); return false; } $this->plugins = new FileDownloadPlugin($this); } } /** * Set class configuration exclusively for multiple snippet calls * @param array $config snippet's parameters */ public function setConfigs($config = array()) { // Clear previous output for subsequent snippet calls $this->_output = array( 'rows' => '', 'dirRows' => '', 'fileRows' => '' ); $config['getDir'] = !empty($config['getDir']) ? $this->_checkPath($config['getDir']) : ''; $config['origDir'] = !empty($config['getDir']) ? $config['getDir'] : ''; // getDir will be overridden by setDirProp() $config['getFile'] = !empty($config['getFile']) ? $this->_checkPath($config['getFile']) : ''; $config = $this->replacePropPhs($config); $this->config = array_merge($this->config, $config); } /** * Define individual config for the class * @param string $key array's key * @param string $val array's value */ public function setConfig($key, $val) { $this->config[$key] = $val; } public function getConfig($key) { return $this->config[$key]; } public function getConfigs() { return $this->config; } /** * Set string error for boolean returned methods * @return void */ public function setError($msg) { $this->_error = $msg; } /** * Get string error for boolean returned methods * @return string output */ public function getError() { return $this->_error; } /** * Set string output for boolean returned methods * @return void */ public function setOutput($msg) { $this->_output = $msg; } /** * Get string output for boolean returned methods * @return string output */ public function getOutput() { return $this->_output; } /** * Set internal placeholder * @param string $key key * @param string $value value * @param string $prefix add prefix if it's required */ public function setPlaceholder($key, $value, $prefix = '') { $prefix = !empty($prefix) ? $prefix : (isset($this->config['phsPrefix']) ? $this->config['phsPrefix'] : ''); $this->_placeholders[$prefix . $key] = $this->trimString($value); } /** * Set internal placeholders * @param array $placeholders placeholders in an associative array * @param string $prefix add prefix if it's required * @return mixed boolean|array of placeholders */ public function setPlaceholders($placeholders, $prefix = '') { if (empty($placeholders)) { return FALSE; } $prefix = !empty($prefix) ? $prefix : (isset($this->config['phsPrefix']) ? $this->config['phsPrefix'] : ''); $placeholders = $this->trimArray($placeholders); $placeholders = $this->implodePhs($placeholders, rtrim($prefix, '.')); // enclosed private scope $this->_placeholders = array_merge($this->_placeholders, $placeholders); // return only for this scope return $placeholders; } /** * Get internal placeholders in an associative array * @return array */ public function getPlaceholders() { return $this->_placeholders; } /** * Get an internal placeholder * @param string $key key * @return string value */ public function getPlaceholder($key) { return $this->_placeholders[$key]; } /** * Merge multi dimensional associative arrays with separator * @param array $array raw associative array * @param string $keyName parent key of this array * @param string $separator separator between the merged keys * @param array $holder to hold temporary array results * @return array one level array */ public function implodePhs(array $array, $keyName = null, $separator = '.', array $holder = array()) { $phs = !empty($holder) ? $holder : array(); foreach ($array as $k => $v) { $key = !empty($keyName) ? $keyName . $separator . $k : $k; if (is_array($v)) { $phs = $this->implodePhs($v, $key, $separator, $phs); } else { $phs[$key] = $v; } } return $phs; } /** * Trim string value * @param string $string source text * @param string $charlist defined characters to be trimmed * @link http://php.net/manual/en/function.trim.php * @return string trimmed text */ public function trimString($string, $charlist = null) { if (empty($string) && !is_numeric($string)) { return ''; } $string = htmlentities($string); // blame TinyMCE! $string = preg_replace('/(Â| )+/i', '', $string); $string = trim($string, $charlist); $string = trim(preg_replace('/\s+^(\r|\n|\r\n)/', ' ', $string)); $string = html_entity_decode($string); return $string; } /** * Trim array values * @param array $array array contents * @param string $charlist [default: null] defined characters to be trimmed * @link http://php.net/manual/en/function.trim.php * @return array trimmed array */ public function trimArray($input, $charlist = null) { if (is_array($input)) { $output = array_map(array($this, 'trimArray'), $input); } else { $output = $this->trimString($input, $charlist); } return $output; } /** * Parsing template * @param string $tpl @BINDINGs options * @param array $phs placeholders * @return string parsed output * @link http://forums.modx.com/thread/74071/help-with-getchunk-and-modx-speed-please?page=2#dis-post-413789 */ public function parseTpl($tpl, array $phs = array()) { $output = ''; if (preg_match('/^(@CODE|@INLINE)/i', $tpl)) { $tplString = preg_replace('/^(@CODE|@INLINE)/i', '', $tpl); // tricks @CODE: / @INLINE: $tplString = ltrim($tplString, ':'); $tplString = trim($tplString); $output = $this->parseTplCode($tplString, $phs); } elseif (preg_match('/^@FILE/i', $tpl)) { $tplFile = preg_replace('/^@FILE/i', '', $tpl); // tricks @FILE: $tplFile = ltrim($tplFile, ':'); $tplFile = trim($tplFile); $tplFile = $this->replacePropPhs($tplFile); try { $output = $this->parseTplFile($tplFile, $phs); } catch (Exception $e) { return $e->getMessage(); } } // ignore @CHUNK / @CHUNK: / empty @BINDING else { $tplChunk = preg_replace('/^@CHUNK/i', '', $tpl); // tricks @CHUNK: $tplChunk = ltrim($tpl, ':'); $tplChunk = trim($tpl); $chunk = $this->modx->getObject('modChunk', array('name' => $tplChunk), true); if (empty($chunk)) { // try to use @splittingred's fallback $f = $this->config['chunksPath'] . strtolower($tplChunk) . '.chunk.tpl'; try { $output = $this->parseTplFile($f, $phs); } catch (Exception $e) { $output = $e->getMessage(); return 'Chunk: ' . $tplChunk . ' is not found, neither the file ' . $output; } } else { // $output = $this->modx->getChunk($tplChunk, $phs); /** * @link http://forums.modx.com/thread/74071/help-with-getchunk-and-modx-speed-please?page=4#dis-post-464137 */ $chunk = $this->modx->getParser()->getElement('modChunk', $tplChunk); $chunk->setCacheable(false); $chunk->_processed = false; $output = $chunk->process($phs); } } return $output; } /** * Parsing inline template code * @param string $code HTML with tags * @param array $phs placeholders * @return string parsed output */ public function parseTplCode($code, array $phs = array()) { $chunk = $this->modx->newObject('modChunk'); $chunk->setContent($code); $chunk->setCacheable(false); $phs = $this->replacePropPhs($phs); $chunk->_processed = false; return $chunk->process($phs); } /** * Parsing file based template * @param string $file file path * @param array $phs placeholders * @return string parsed output * @throws Exception if file is not found */ public function parseTplFile($file, array $phs = array()) { if (!file_exists($file)) { throw new Exception('File: ' . $file . ' is not found.'); } $o = file_get_contents($file); $chunk = $this->modx->newObject('modChunk'); // just to create a name for the modChunk object. $name = strtolower(basename($file)); $name = rtrim($name, '.tpl'); $name = rtrim($name, '.chunk'); $chunk->set('name', $name); $chunk->setCacheable(false); $chunk->setContent($o); $chunk->_processed = false; $output = $chunk->process($phs); return $output; } /** * If the chunk is called by AJAX processor, it needs to be parsed for the * other elements to work, like snippet and output filters. * * Example: *
* parseTpl('tplName', $placeholders);
* $content = $myObject->processElementTags($content);
*
*
* @param string $content the chunk output
* @param array $options option for iteration
* @return string parsed content
*/
public function processElementTags($content, array $options = array()) {
$maxIterations = intval($this->modx->getOption('parser_max_iterations', $options, 10));
if (!$this->modx->parser) {
$this->modx->getParser();
}
$this->modx->parser->processElementTags('', $content, true, false, '[[', ']]', array(), $maxIterations);
$this->modx->parser->processElementTags('', $content, true, true, '[[', ']]', array(), $maxIterations);
return $content;
}
/**
* Replace the property's placeholders
* @param string|array $subject Property
* @return array The replaced results
*/
public function replacePropPhs($subject) {
$pattern = array(
'/\{core_path\}/',
'/\{base_path\}/',
'/\{assets_url\}/',
'/\{filemanager_path\}/',
'/\[\[\+\+core_path\]\]/',
'/\[\[\+\+base_path\]\]/'
);
$replacement = array(
$this->modx->getOption('core_path'),
$this->modx->getOption('base_path'),
$this->modx->getOption('assets_url'),
$this->modx->getOption('filemanager_path'),
$this->modx->getOption('core_path'),
$this->modx->getOption('base_path')
);
if (is_array($subject)) {
$parsedString = array();
foreach ($subject as $k => $s) {
if (is_array($s)) {
$s = $this->replacePropPhs($s);
}
$parsedString[$k] = preg_replace($pattern, $replacement, $s);
}
return $parsedString;
} else {
return preg_replace($pattern, $replacement, $subject);
}
}
/**
* Get the clean path array and clean up some duplicate slashes
* @param string $paths multiple paths with comma separated
* @return array Dir paths in an array
*/
private function _checkPath($paths) {
$forbiddenFolders = array(
realpath(MODX_CORE_PATH),
realpath(MODX_PROCESSORS_PATH),
realpath(MODX_CONNECTORS_PATH),
realpath(MODX_MANAGER_PATH),
realpath(MODX_BASE_PATH)
);
$cleanPaths = array();
if (!empty($paths)) {
$xPath = @explode(',', $paths);
foreach ($xPath as $path) {
if (empty($path)) {
continue;
}
$path = $this->trimString($path);
$realpath = realpath($path);
if (empty($realpath)) {
$realpath = realpath(MODX_BASE_PATH . $path);
if (empty($realpath)) {
continue;
}
}
if (in_array($realpath, $forbiddenFolders)) {
continue;
}
$cleanPaths[] = $realpath;
}
}
return $cleanPaths;
}
/**
* View any string as a hexdump.
*
* This is most commonly used to view binary data from streams
* or sockets while debugging, but can be used to view any string
* with non-viewable characters.
*
* @version 1.3.2
* @author Aidan Lister ' : '';
$offset = 0;
$len = strlen($data);
// Upper or lower case hexadecimal
$x = ($uppercase === false) ? 'x' : 'X';
// Iterate string
for ($i = $j = 0; $i < $len; $i++) {
// Convert to hexidecimal
$hexi .= sprintf("%02$x ", ord($data[$i]));
// Replace non-viewable bytes with '.'
if (ord($data[$i]) >= 32) {
$ascii .= ($htmloutput === true) ?
htmlentities($data[$i]) :
$data[$i];
} else {
$ascii .= '.';
}
// Add extra column spacing
if ($j === 7) {
$hexi .= ' ';
$ascii .= ' ';
}
// Add row
if (++$j === 16 || $i === $len - 1) {
// Join the hexi / ascii output
$dump .= sprintf("%04$x %-49s %s", $offset, $hexi, $ascii);
// Reset vars
$hexi = $ascii = '';
$offset += 16;
$j = 0;
// Add newline
if ($i !== $len - 1) {
$dump .= "\n";
}
}
}
// Finish dump
$dump .= $htmloutput === true ?
'' :
'';
$dump .= "\n";
// Output method
if ($return === false) {
echo $dump;
} else {
return $dump;
}
}
/**
* Retrieve the content of the given path
* @param mixed $root The specified root path
* @return array All contents in an array
*/
public function getContents() {
$plugins = $this->getPlugins('OnLoad', $this->config);
if ($plugins === FALSE) { // strict detection
return FALSE;
}
$dirContents = array();
if (!empty($this->config['getDir'])) {
$dirContents = $this->_getDirContents($this->config['getDir']);
if (!$dirContents)
$dirContents = array();
}
$fileContents = array();
if (!empty($this->config['getFile'])) {
$fileContents = $this->_getFileContents($this->config['getFile']);
if (!$fileContents)
$fileContents = array();
}
$mergedContents = array();
$mergedContents = array_merge($dirContents, $fileContents);
$mergedContents = $this->_checkDuplication($mergedContents);
$mergedContents = $this->_getDescription($mergedContents);
$mergedContents = $this->_sortOrder($mergedContents);
return $mergedContents;
}
/**
* Existed description from the chunk of the &chkDesc parameter
* @param array $contents
*/
private function _getDescription(array $contents) {
if (empty($contents)) {
return $contents;
}
if (empty($this->config['chkDesc'])) {
foreach ($contents as $key => $file) {
$contents[$key]['description'] = '';
}
return $contents;
}
$chunkContent = $this->modx->getChunk($this->config['chkDesc']);
$linesX = @explode('||', $chunkContent);
array_walk($linesX, create_function('&$val', '$val = trim($val);'));
foreach ($linesX as $k => $v) {
if (empty($v)) {
unset($linesX[$k]);
continue;
}
$descX = @explode('|', $v);
array_walk($descX, create_function('&$val', '$val = trim($val);'));
$phsReplaced = $this->replacePropPhs($descX[0]);
$realPath = realpath($phsReplaced);
if (!$realPath) {
continue;
}
$desc[$realPath] = $descX[1];
}
foreach ($contents as $key => $file) {
$contents[$key]['description'] = '';
if (isset($desc[$file['fullPath']])) {
$contents[$key]['description'] = $desc[$file['fullPath']];
}
}
return $contents;
}
/**
* Check the called file contents with the registered database.
* If it's not listed, auto save
* @param array $file Realpath filename / dirname
* @return void
*/
private function _checkDb(array $file) {
if (empty($file)) {
return FALSE;
}
$realPath = realpath($file['filename']);
if (empty($realPath)) {
return FALSE;
}
$fdlObj = $this->modx->getObject('FDL', array(
'ctx' => $file['ctx'],
'filename' => utf8_encode($file['filename'])
));
$checked = array();
if ($fdlObj === null) {
$fdlObj = $this->modx->newObject('FDL');
$fdlObj->fromArray(array(
'ctx' => $file['ctx'],
'filename' => utf8_encode($file['filename']),
'count' => 0,
'hash' => $this->_setHashedParam($file['ctx'], $file['filename'])
));
$fdlObj->save();
$checked['ctx'] = $fdlObj->get('ctx');
$checked['filename'] = $fdlObj->get('filename');
$checked['count'] = $fdlObj->get('count');
$checked['hash'] = $fdlObj->get('hash');
return $checked;
} else {
$checked['ctx'] = $fdlObj->get('ctx');
$checked['filename'] = $fdlObj->get('filename');
$checked['count'] = $fdlObj->get('count');
$checked['hash'] = $fdlObj->get('hash');
return $checked;
}
return FALSE;
}
/**
* Check any duplication output
* @param array $mergedContents merging the &getDir and &getFile result
* @return array Unique filenames
*/
private function _checkDuplication(array $mergedContents) {
if (empty($mergedContents)) {
return $mergedContents;
}
$this->_count['dirs'] = 0;
$this->_count['files'] = 0;
$c = array();
$d = array();
foreach ($mergedContents as $content) {
if (isset($c[$content['fullPath']]))
continue;
$c[$content['fullPath']] = $content;
$d[] = $content;
if ($content['type'] === 'dir') {
$this->_count['dirs']++;
} else {
$this->_count['files']++;
}
}
return $d;
}
/**
* Count the numbers retrieved objects (dirs/files)
* @param string $subject the specified subject
* @return int number of the subject
*/
public function countContents($subject) {
if ($subject === 'dirs') {
return $this->_count['dirs'];
} elseif ($subject === 'files') {
return $this->_count['files'];
} else {
return intval(0);
}
}
/**
* Load UTF-8 Class
* @param string $callback method's name
* @param array $callbackParams call back parameters (in an array)
* @author Rin
* @link http://forum.dklab.ru/viewtopic.php?p=91015#91015
* @return string converted text
*/
private function _utfRin($callback, array $callbackParams = array()) {
include_once(dirname(dirname(dirname(__FILE__))) . '/includes/UTF8-2.1.1/UTF8.php');
include_once(dirname(dirname(dirname(__FILE__))) . '/includes/UTF8-2.1.1/ReflectionTypehint.php');
$utf = call_user_func_array(array('UTF8', $callback), $callbackParams);
return $utf;
}
/**
* Retrieve the content of the given directory path
* @param array $paths The specified root path
* @return array Dir's contents in an array
*/
private function _getDirContents(array $paths = array()) {
if (empty($paths)) {
return FALSE;
}
$contents = array();
foreach ($paths as $rootPath) {
if (!is_dir($rootPath)) {
// @todo: lexicon
$this->modx->log(
modX::LOG_LEVEL_ERROR, '&getDir parameter expects a correct dir path. "' . $rootPath . '" is given.'
);
return FALSE;
}
$plugins = $this->getPlugins('BeforeDirOpen', array(
'dirPath' => $rootPath,
));
if ($plugins === FALSE) { // strict detection
return FALSE;
} elseif ($plugins === 'continue') {
continue;
}
$scanDir = scandir($rootPath);
foreach ($scanDir as $file) {
if ($file === '.' || $file === '..' || $file === 'Thumbs.db' || $file === '.htaccess' || $file === '.htpasswd'
) {
continue;
}
$rootRealPath = realpath($rootPath);
if (!$rootRealPath) {
return FALSE;
}
$fullPath = $rootRealPath . DIRECTORY_SEPARATOR . $file;
$fileType = @filetype($fullPath);
if ($fileType == 'file') {
$fileInfo = $this->_fileInformation($fullPath);
if (!$fileInfo) {
continue;
}
$contents[] = $fileInfo;
} elseif ($this->config['browseDirectories']) {
// a directory
$cdb['ctx'] = $this->modx->context->key;
$cdb['filename'] = $fullPath;
$cdb['count'] = $this->_getDownloadCount($cdb['ctx'], $cdb['filename']);
$cdb['hash'] = $this->_getHashedParam($cdb['ctx'], $cdb['filename']);
$checkedDb = $this->_checkDb($cdb);
if (!$checkedDb) {
continue;
}
$notation = $this->_aliasName($file);
$alias = $notation[1];
$unixDate = filemtime($fullPath);
$date = date($this->config['dateFormat'], $unixDate);
$link = $this->_linkDirOpen($checkedDb['hash'], $checkedDb['ctx']);
$imgType = $this->_imgType('dir');
$dir = array(
'ctx' => $checkedDb['ctx'],
'fullPath' => utf8_encode($fullPath),
'path' => utf8_encode($rootRealPath),
'filename' => utf8_encode($file),
'alias' => utf8_encode($alias),
'type' => $fileType,
'ext' => '',
'size' => '',
'sizeText' => '',
'unixdate' => $unixDate,
'date' => $date,
'image' => $this->config['imgTypeUrl'] . $imgType,
'count' => $checkedDb['count'],
'link' => $link['url'], // fallback
'url' => $link['url'],
'hash' => $checkedDb['hash']
);
$contents[] = $dir;
}
}
$plugins = $this->getPlugins('AfterDirOpen', array(
'dirPath' => $rootPath,
'contents' => $contents,
));
if ($plugins === FALSE) { // strict detection
return FALSE;
} elseif ($plugins === 'continue') {
continue;
}
}
return $contents;
}
/**
* Retrieve the content of the given file path
* @param array $paths The specified file path
* @return array File contents in an array
*/
private function _getFileContents(array $paths = array()) {
$contents = array();
foreach ($paths as $fileRow) {
$fileInfo = $this->_fileInformation($fileRow);
if (!$fileInfo) {
continue;
}
$contents[] = $fileInfo;
}
return $contents;
}
/**
* Retrieves the required information from a file
* @param string $file absoulte file path or a file with an [| alias]
* @return array All about the file
*/
private function _fileInformation($file) {
$notation = $this->_aliasName($file);
$path = $notation[0];
$alias = $notation[1];
$fileRealPath = realpath($path);
if (!is_file($fileRealPath) || !$fileRealPath) {
// @todo: lexicon
$this->modx->log(
modX::LOG_LEVEL_ERROR, '&getFile parameter expects a correct file path. ' . $path . ' is given.'
);
return FALSE;
}
$baseName = basename($fileRealPath);
$xBaseName = explode('.', $baseName);
$tempExt = end($xBaseName);
$ext = strtolower($tempExt);
$size = filesize($fileRealPath);
$imgType = $this->_imgType($ext);
if (!$this->_isExtShown($ext)) {
return FALSE;
}
if ($this->_isExtHidden($ext)) {
return FALSE;
}
$cdb['ctx'] = $this->modx->context->key;
$cdb['filename'] = $fileRealPath;
$cdb['count'] = $this->_getDownloadCount($cdb['ctx'], $cdb['filename']);
$cdb['hash'] = $this->_getHashedParam($cdb['ctx'], $cdb['filename']);
$checkedDb = $this->_checkDb($cdb);
if (!$checkedDb) {
return FALSE;
}
if ($this->config['directLink']) {
$link = $this->_directLinkFileDownload(utf8_decode($checkedDb['filename']));
if (!$link)
return FALSE;
} else {
$link = $this->_linkFileDownload($checkedDb['filename'], $checkedDb['hash'], $checkedDb['ctx']);
}
$unixDate = filemtime($fileRealPath);
$date = date($this->config['dateFormat'], $unixDate);
$info = array(
'ctx' => $checkedDb['ctx'],
'fullPath' => $fileRealPath,
'path' => utf8_encode(dirname($fileRealPath)),
'filename' => utf8_encode($baseName),
'alias' => $alias,
'type' => filetype($fileRealPath),
'ext' => $ext,
'size' => $size,
'sizeText' => $this->_fileSizeText($size),
'unixdate' => $unixDate,
'date' => $date,
'image' => $this->config['imgTypeUrl'] . $imgType,
'count' => $checkedDb['count'],
'link' => $link['url'], // fallback
'url' => $link['url'],
'hash' => $checkedDb['hash']
);
return $info;
}
/**
* Get the alias/description from the pipe ( "|" ) symbol on the snippet
* @param string $path the full path
* @return array [0] => the path [1] => the alias name
*/
private function _aliasName($path) {
$xPipes = @explode('|', $path);
$notation = array();
$notation[0] = trim($xPipes[0]);
$notation[1] = !isset($xPipes[1]) ? '' : trim($xPipes[1]);
return $notation;
}
/**
* Get the right image type to the specified file's extension, or fall back
* to the default image.
* @param string $ext
* @return type
*/
private function _imgType($ext) {
return isset($this->_imgType[$ext]) ? $this->_imgType[$ext] : (isset($this->_imgType['default']) ? $this->_imgType['default'] : FALSE);
}
/**
* Retrieve the images for the specified file extensions
* @return array file type's images
*/
private function _imgTypeProp() {
if (empty($this->config['imgLocat'])) {
return FALSE;
}
$fdImagesChunk = $this->parseTpl($this->config['imgTypes']);
$fdImagesChunkX = @explode(',', $fdImagesChunk);
$imgType = array();
foreach ($fdImagesChunkX as $v) {
$typeX = @explode('=', $v);
$imgType[strtolower(trim($typeX[0]))] = trim($typeX[1]);
}
return $imgType;
}
/**
* @todo _linkFileDownload: change the hard coded html to template
* @param string $filePath file's path
* @param string $hash hash
* @param string $ctx specifies a context to limit URL generation to.
* @return array the download link and the javascript's attribute
*/
private function _linkFileDownload($filePath, $hash, $ctx = 'web') {
$link = array();
if ($this->config['noDownload']) {
$link['url'] = $filePath;
} else {
$args = 'fdlfile=' . $hash;
$url = $this->modx->makeUrl($this->modx->resource->get('id'), $ctx, $args);
$link['url'] = $url;
}
$link['hash'] = $hash;
return $link;
}
/**
* Set the direct link to the file path
* @param string $filePath absolute file path
* @return array the download link and the javascript's attribute
*/
private function _directLinkFileDownload($filePath) {
$link = array();
if ($this->config['noDownload']) {
$link['url'] = $filePath;
} else {
// to use this method, the file should always be placed on the web root
$corePath = str_replace('/', DIRECTORY_SEPARATOR, MODX_CORE_PATH);
if (stristr($filePath, $corePath)) {
return FALSE;
}
// switching from absolute path to url is nuts
$fileUrl = str_ireplace(MODX_BASE_PATH, MODX_SITE_URL, $filePath);
$fileUrl = str_replace(DIRECTORY_SEPARATOR, '/', $fileUrl);
$parseUrl = parse_url($fileUrl);
$url = ltrim($parseUrl['path'], '/' . MODX_HTTP_HOST);
$link['url'] = MODX_URL_SCHEME . MODX_HTTP_HOST . '/' . $url;
}
$link['hash'] = '';
return $link;
}
/**
* @todo _linkDirOpen: change the hard coded html to template
* @param string $hash hash
* @param string $ctx specifies a context to limit URL generation to.
* @return array the open directory link and the javascript's attribute
*/
private function _linkDirOpen($hash, $ctx = 'web') {
if (!$this->config['browseDirectories']) {
return FALSE;
}
$link = array();
$args = 'fdldir=' . $hash;
if (!empty($this->config['fdlid'])) {
$args .= '&fdlid=' . $this->config['fdlid'];
}
$url = $this->modx->makeUrl($this->modx->resource->get('id'), $ctx, $args);
$link['url'] = $url;
$link['hash'] = $hash;
return $link;
}
/**
* Set the new value to the getDir property to browse inside the clicked
* directory
* @param string $hash the hashed link
* @param bool $selected to patch multiple snippet call
* @return bool TRUE | FALSE
*/
public function setDirProp($hash, $selected = true) {
if (empty($hash) || !$selected) {
return FALSE;
}
$fdlObj = $this->modx->getObject('FDL', array('hash' => $hash));
if (!$fdlObj) {
return FALSE;
}
$ctx = $fdlObj->get('ctx');
$path = $fdlObj->get('filename');
$count = $fdlObj->get('count');
if ($this->modx->context->key !== $ctx) {
return FALSE;
}
$this->config['getDir'] = array($path);
$this->config['getFile'] = array();
// save the new count
$newCount = $count + 1;
$fdlObj->set('count', $newCount);
if ($fdlObj->save() === false) {
// @todo setDirProp: lexicon string
return $this->modx->error->failure($this->modx->lexicon($this->config['prefix'] . 'err_save_counter'));
}
return TRUE;
}
/**
* Download action
* @param string $hash hashed text
* @return void file is pulled to the browser
*/
public function downloadFile($hash) {
if (empty($hash)) {
return FALSE;
}
$fdlObj = $this->modx->getObject('FDL', array('hash' => $hash));
if (!$fdlObj) {
return FALSE;
}
$ctx = $fdlObj->get('ctx');
$filePath = utf8_decode($fdlObj->get('filename'));
$count = $fdlObj->get('count');
if ($this->modx->context->key !== $ctx) {
return FALSE;
}
$plugins = $this->getPlugins('BeforeFileDownload', array(
'hash' => $hash,
'ctx' => $ctx,
'filePath' => $filePath,
'count' => $count,
));
if ($plugins === FALSE) { // strict detection
return FALSE;
}
if (file_exists($filePath)) {
// required for IE
if (ini_get('zlib.output_compression')) {
ini_set('zlib.output_compression', 'Off');
}
@set_time_limit(300);
@ini_set('magic_quotes_runtime', 0);
ob_end_clean(); //added to fix ZIP file corruption
ob_start(); //added to fix ZIP file corruption
header('Pragma: public'); // required
header('Expires: 0'); // no cache
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Cache-Control: private', false);
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', filemtime($filePath)) . ' GMT');
header('Content-Description: File Transfer');
header('Content-Type:'); //added to fix ZIP file corruption
header('Content-Type: "application/force-download"');
header('Content-Disposition: attachment; filename="' . basename($filePath) . '"');
header('Content-Transfer-Encoding: binary');
header('Content-Length: ' . (string) (filesize($filePath))); // provide file size
header('Connection: close');
sleep(1);
//Close the session to allow for header() to be sent
session_write_close();
ob_flush();
flush();
$chunksize = 1 * (1024 * 1024); // how many bytes per chunk
$buffer = '';
$handle = @fopen($filePath, 'rb');
if ($handle === false) {
return false;
}
while (!feof($handle) && connection_status() == 0) {
$buffer = @fread($handle, $chunksize);
if (!$buffer) {
die();
}
echo $buffer;
ob_flush();
flush();
}
fclose($handle);
if ($this->config['countDownloads']) {
// save the new count
$newCount = $count + 1;
$fdlObj->set('count', $newCount);
if ($fdlObj->save() === false) {
// @todo downloadFile: lexicon string
return $this->modx->error->failure($this->modx->lexicon('filedownload.fdl_err_save'));
}
}
// just run this away, it doesn't matter if the return is FALSE
$this->getPlugins('AfterFileDownload', array(
'hash' => $hash,
'ctx' => $ctx,
'filePath' => $filePath,
'count' => $newCount,
));
exit;
}
return FALSE;
}
/**
* Get the download counting for the specified file and context
* @param type $ctx
* @param type $filePath
* @return type
*/
private function _getDownloadCount($ctx, $filePath) {
$fdlObj = $this->modx->getObject('FDL', array(
'ctx' => $ctx,
'filename' => $filePath
));
if (!$fdlObj)
return '';
return $fdlObj->get('count');
}
/**
* Check whether the file with the specified extension is hidden from the list
* @param string $ext file's extension
* @return bool TRUE | FALSE
*/
private function _isExtHidden($ext) {
if (empty($this->config['extHidden'])) {
return FALSE;
}
$extHiddenX = @explode(',', $this->config['extHidden']);
array_walk($extHiddenX, create_function('&$val', '$val = strtolower(trim($val));'));
if (!in_array($ext, $extHiddenX)) {
return TRUE;
} else {
return FALSE;
}
}
/**
* Check whether the file with the specified extension is shown to the list
* @param string $ext file's extension
* @return bool TRUE | FALSE
*/
private function _isExtShown($ext) {
if (empty($this->config['extShown'])) {
return TRUE;
}
$extShownX = @explode(',', $this->config['extShown']);
array_walk($extShownX, create_function('&$val', '$val = strtolower(trim($val));'));
if (in_array($ext, $extShownX)) {
return TRUE;
} else {
return FALSE;
}
}
/**
* Check the user's group
* @param void
* @return bool TRUE | FALSE
*/
public function isAllowed() {
if (empty($this->config['userGroups'])) {
return TRUE;
} else {
$userGroupsX = @explode(',', $this->config['userGroups']);
array_walk($userGroupsX, create_function('&$val', '$val = trim($val);'));
$userAccessGroupNames = $this->_userAccessGroupNames();
$intersect = array_uintersect($userGroupsX, $userAccessGroupNames, "strcasecmp");
if (count($intersect) > 0) {
return TRUE;
} else {
return FALSE;
}
}
}
/**
* Get logged in usergroup names
* @return array access group names
*/
private function _userAccessGroupNames() {
$userAccessGroupNames = array();
$userId = $this->modx->user->get('id');
if (empty($userId)) {
return $userAccessGroupNames;
}
$userObj = $this->modx->getObject('modUser', $userId);
$userGroupObj = $userObj->getMany('UserGroupMembers');
foreach ($userGroupObj as $uGO) {
$userGroupNameObj = $this->modx->getObject('modUserGroup', $uGO->get('user_group'));
$userAccessGroupNames[] = $userGroupNameObj->get('name');
}
return $userAccessGroupNames;
}
/**
* Prettify the file size with thousands unit byte
* @param int $fileSize filesize()
* @return string the pretty number
*/
private function _fileSizeText($fileSize) {
if ($fileSize === 0) {
$returnVal = '0 bytes';
} else if ($fileSize > 1024 * 1024 * 1024) {
$returnVal = (ceil($fileSize / (1024 * 1024 * 1024) * 100) / 100) . ' GB';
} else if ($fileSize > 1024 * 1024) {
$returnVal = (ceil($fileSize / (1024 * 1024) * 100) / 100) . ' MB';
} else if ($fileSize > 1024) {
$returnVal = (ceil($fileSize / 1024 * 100) / 100) . ' kB';
} else {
$returnVal = $fileSize . ' B';
}
return $returnVal;
}
/**
* Manage the order sorting by all sorting parameters:
* - sortBy
* - sortOrder
* - sortOrderNatural
* - sortByCaseSensitive
* - browseDirectories
* - groupByDirectory
* @param array $contents unsorted contents
* @return array sorted contents
*/
private function _sortOrder(array $contents) {
if (empty($contents)) {
return $contents;
} else {
$sort = $contents;
}
if (empty($this->config['groupByDirectory'])) {
$sort = $this->_groupByType($contents);
} else {
$sortPath = array();
foreach ($contents as $k => $file) {
if (!$this->config['browseDirectories'] && $file['type'] === 'dir') {
continue;
}
$sortPath[$file['path']][$k] = $file;
}
$sort = array();
foreach ($sortPath as $k => $path) {
// path name for the &groupByDirectory template: tpl-group
$this->_output['rows'] .= $this->_tplDirectory($k);
$sort['path'][$k] = $this->_groupByType($path);
}
}
return $sort;
}
/**
* Grouping the contents by filetype
* @param array $contents contents
* @return array grouped contents
*/
private function _groupByType(array $contents) {
if (empty($contents)) {
return FALSE;
}
$sortType = array();
foreach ($contents as $k => $file) {
if (empty($this->config['browseDirectories']) && $file['type'] === 'dir') {
continue;
}
$sortType[$file['type']][$k] = $file;
}
if (empty($sortType)) {
return FALSE;
}
foreach ($sortType as $k => $file) {
if (count($file) > 1) {
$sortType[$k] = $this->_sortMultiOrders($file);
}
}
$sort = array();
$dirs = '';
if (!empty($this->config['browseDirectories']) && !empty($sortType['dir'])) {
$sort['dir'] = $sortType['dir'];
// template
$row = 1;
foreach ($sort['dir'] as $k => $v) {
$v['class'] = $this->_cssDir($row);
$dirs .= $this->_tplDir($v);
$row++;
}
}
$phs = array();
$phs[$this->config['prefix'] . 'classPath'] = (!empty($this->config['cssPath'])) ? ' class="' . $this->config['cssPath'] . '"' : '';
$phs[$this->config['prefix'] . 'path'] = $this->_breadcrumbs();
if (!empty($this->config['tplWrapperDir']) && !empty($dirs)) {
$phs[$this->config['prefix'] . 'dirRows'] = $dirs;
$this->_output['dirRows'] .= $this->parseTpl($this->config['tplWrapperDir'], $phs);
} else {
$this->_output['dirRows'] .= $dirs;
}
$files = '';
if (!empty($sortType['file'])) {
$sort['file'] = $sortType['file'];
// template
$row = 1;
foreach ($sort['file'] as $k => $v) {
$v['class'] = $this->_cssFile($row, $v['ext']);
$files .= $this->_tplFile($v);
$row++;
}
}
if (!empty($this->config['tplWrapperFile']) && !empty($files)) {
$phs[$this->config['prefix'] . 'fileRows'] = $files;
$this->_output['fileRows'] .= $this->parseTpl($this->config['tplWrapperFile'], $phs);
} else {
$this->_output['fileRows'] .= $files;
}
$this->_output['rows'] .= $this->_output['dirRows'];
$this->_output['rows'] .= $this->_output['fileRows'];
return $sort;
}
/**
* Multi dimensional sorting
* @param array $array content array
* @param string $index order index
* @param string $order asc [| void]
* @param bool $natSort TRUE | FALSE
* @param bool $caseSensitive TRUE | FALSE
* @return array the sorted array
* @link modified from http://www.php.net/manual/en/function.sort.php#104464
*/
private function _sortMultiOrders($array) {
if (!is_array($array) || count($array) < 1) {
return $array;
}
$temp = array();
foreach (array_keys($array) as $key) {
$temp[$key] = $array[$key][$this->config['sortBy']];
}
if ($this->config['sortOrderNatural'] != 1) {
if (strtolower($this->config['sortOrder']) == 'asc') {
asort($temp);
} else {
arsort($temp);
}
} else {
if ($this->config['sortByCaseSensitive'] != 1) {
natcasesort($temp);
} else {
natsort($temp);
}
if (strtolower($this->config['sortOrder']) != 'asc') {
$temp = array_reverse($temp, TRUE);
}
}
$sorted = array();
foreach (array_keys($temp) as $key) {
if (is_numeric($key)) {
$sorted[] = $array[$key];
} else {
$sorted[$key] = $array[$key];
}
}
return $sorted;
}
/**
* Generate the class names for the directory rows
* @param int $row the row number
* @return string imploded class names
*/
private function _cssDir($row) {
$totalRow = $this->_count['dirs'];
$cssName = array();
if (!empty($this->config['cssDir'])) {
$cssName[] = $this->config['cssDir'];
}
if (!empty($this->config['cssAltRow']) && $row % 2 === 1) {
$cssName[] = $this->config['cssAltRow'];
}
if (!empty($this->config['cssFirstDir']) && $row === 1) {
$cssName[] = $this->config['cssFirstDir'];
} elseif (!empty($this->config['cssLastDir']) && $row === $totalRow) {
$cssName[] = $this->config['cssLastDir'];
}
$o = '';
$cssNames = @implode(' ', $cssName);
if (!empty($cssNames)) {
$o = ' class="' . $cssNames . '"';
}
return $o;
}
/**
* Generate the class names for the file rows
* @param int $row the row number
* @param string $ext extension
* @return string imploded class names
*/
private function _cssFile($row, $ext) {
$totalRow = $this->_count['files'];
$cssName = array();
if (!empty($this->config['cssFile'])) {
$cssName[] = $this->config['cssFile'];
}
if (!empty($this->config['cssAltRow']) && $row % 2 === 1) {
if ($this->_count['dirs'] % 2 === 0) {
$cssName[] = $this->config['cssAltRow'];
}
}
if (!empty($this->config['cssFirstFile']) && $row === 1) {
$cssName[] = $this->config['cssFirstFile'];
} elseif (!empty($this->config['cssLastFile']) && $row === $totalRow) {
$cssName[] = $this->config['cssLastFile'];
}
if (!empty($this->config['cssExtension'])) {
$cssNameExt = '';
if (!empty($this->config['cssExtensionPrefix'])) {
$cssNameExt .= $this->config['cssExtensionPrefix'];
}
$cssNameExt .= $ext;
if (!empty($this->config['cssExtensionSuffix'])) {
$cssNameExt .= $this->config['cssExtensionSuffix'];
}
$cssName[] = $cssNameExt;
}
$o = '';
$cssNames = @implode(' ', $cssName);
if (!empty($cssNames)) {
$o = ' class="' . $cssNames . '"';
}
return $o;
}
/**
* Parsing the directory template
* @param array $contents properties
* @return string rendered HTML
*/
private function _tplDir(array $contents) {
if (empty($contents)) {
return '';
}
foreach ($contents as $k => $v) {
$phs[$this->config['prefix'] . $k] = $v;
}
$tpl = $this->parseTpl($this->config['tplDir'], $phs);
return $tpl;
}
/**
* Parsing the file template
* @param array $fileInfo properties
* @return string rendered HTML
*/
private function _tplFile(array $fileInfo) {
if (empty($fileInfo) || empty($this->config['tplFile'])) {
return '';
}
foreach ($fileInfo as $k => $v) {
$phs[$this->config['prefix'] . $k] = $v;
}
$tpl = $this->parseTpl($this->config['tplFile'], $phs);
return $tpl;
}
/**
* Path template if &groupByDirectory is enabled
* @param string $path Path's name
* @return string rendered HTML
*/
private function _tplDirectory($path) {
if (empty($path) || is_array($path)) {
return '';
}
$phs[$this->config['prefix'] . 'class'] = (!empty($this->config['cssGroupDir'])) ? ' class="' . $this->config['cssGroupDir'] . '"' : '';
$groupPath = str_replace(DIRECTORY_SEPARATOR, $this->config['breadcrumbSeparator'], $this->_trimPath($path));
$phs[$this->config['prefix'] . 'groupDirectory'] = $groupPath;
$tpl = $this->parseTpl($this->config['tplGroupDir'], $phs);
return $tpl;
}
/**
* Wraps templates
* @return string rendered template
*/
private function _tplWrapper() {
$phs[$this->config['prefix'] . 'classPath'] = (!empty($this->config['cssPath'])) ? ' class="' . $this->config['cssPath'] . '"' : '';
$phs[$this->config['prefix'] . 'path'] = $this->_breadcrumbs();
$rows = !empty($this->_output['rows']) ? $this->_output['rows'] : '';
$phs[$this->config['prefix'] . 'rows'] = $rows;
$phs[$this->config['prefix'] . 'dirRows'] = $this->_output['dirRows'];
$phs[$this->config['prefix'] . 'fileRows'] = $this->_output['fileRows'];
if (!empty($this->config['tplWrapper'])) {
$tpl = $this->parseTpl($this->config['tplWrapper'], $phs);
} else {
$tpl = $rows;
}
return $tpl;
}
/**
* Trim the absolute path to be a relatively safe path
* @param string $path the absolute path
* @return string trimmed path
*/
private function _trimPath($path) {
$xPath = @explode(DIRECTORY_SEPARATOR, $this->config['origDir'][0]);
array_pop($xPath);
$parentPath = @implode(DIRECTORY_SEPARATOR, $xPath) . DIRECTORY_SEPARATOR;
$trimmedPath = $path;
if (FALSE !== stristr($trimmedPath, $parentPath)) {
$trimmedPath = str_replace($parentPath, '', $trimmedPath);
}
$modxCorePath = realpath(MODX_CORE_PATH) . DIRECTORY_SEPARATOR;
$modxAssetsPath = realpath(MODX_ASSETS_PATH) . DIRECTORY_SEPARATOR;
if (FALSE !== stristr($trimmedPath, $modxCorePath)) {
$trimmedPath = str_replace($modxCorePath, '', $trimmedPath);
} elseif (FALSE !== stristr($trimmedPath, $modxAssetsPath)) {
$trimmedPath = str_replace($modxAssetsPath, '', $trimmedPath);
}
return $trimmedPath;
}
/**
* Create a breadcrumbs link
* @param void
* @return string a breadcrumbs link
*/
private function _breadcrumbs() {
if (empty($this->config['browseDirectories'])) {
return '';
}
$dirs = $this->config['getDir'];
if (count($dirs) > 1) {
return '';
} else {
$path = $dirs[0];
}
$trimmedPath = trim($this->_trimPath($path), DIRECTORY_SEPARATOR);
$basePath = str_replace($trimmedPath, '', $path);
$trimmedPathX = @explode(DIRECTORY_SEPARATOR, $trimmedPath);
$trailingPath = $basePath;
$trail = array();
$trailingLink = array();
$countTrimmedPathX = count($trimmedPathX);
foreach ($trimmedPathX as $k => $title) {
$trailingPath .= $title . DIRECTORY_SEPARATOR;
$fdlObj = $this->modx->getObject('FDL', array(
'filename' => $trailingPath
));
if (!$fdlObj) {
$cdb = array();
$cdb['ctx'] = $this->modx->context->key;
$cdb['filename'] = $trailingPath;
$checkedDb = $this->_checkDb($cdb);
if (!$checkedDb) {
continue;
}
$fdlObj = $this->modx->getObject('FDL', array(
'filename' => $trailingPath
));
}
$hash = $fdlObj->get('hash');
$link = $this->_linkDirOpen($hash, $this->modx->context->key);
if ($k === 0) {
$pageUrl = $this->modx->makeUrl($this->modx->resource->get('id'));
$trail[$k] = array(
$this->config['prefix'] . 'title' => $this->modx->lexicon($this->config['prefix'] . 'breadcrumb.home'),
$this->config['prefix'] . 'link' => $pageUrl,
$this->config['prefix'] . 'url' => $pageUrl,
$this->config['prefix'] . 'hash' => '',
);
} else {
$trail[$k] = array(
$this->config['prefix'] . 'title' => $title,
$this->config['prefix'] . 'link' => $link['url'], // fallback
$this->config['prefix'] . 'url' => $link['url'],
$this->config['prefix'] . 'hash' => $hash,
);
}
if ($k < ($countTrimmedPathX - 1)) {
$trailingLink[] = $this->parseTpl($this->config['tplBreadcrumb'], $trail[$k]);
} else {
$trailingLink[] = $title;
}
}
$breadcrumb = @implode($this->config['breadcrumbSeparator'], $trailingLink);
return $breadcrumb;
}
public function parseTemplate() {
$o = $this->_tplWrapper();
return $o;
}
/**
* Sets the salted parameter to the database
* @param string $ctx context
* @param string $filename filename
* @return string hashed parameter
*/
private function _setHashedParam($ctx, $filename) {
$input = $this->config['saltText'] . $ctx . $filename;
return str_rot13(base64_encode(hash('sha512', $input)));
}
/**
* Gets the salted parameter from the System Settings + stored hashed parameter.
* @param string $ctx context
* @param string $filename filename
* @return string hashed parameter
*/
private function _getHashedParam($ctx, $filename) {
$fdlObj = $this->modx->getObject('FDL', array(
'ctx' => $ctx,
'filename' => $filename
));
if (!$fdlObj) {
return FALSE;
}
return $fdlObj->get('hash');
}
/**
* Check whether the REQUEST parameter exists in the database.
* @param string $ctx context
* @param string $hash hash value
* @return bool TRUE | FALSE
*/
public function checkHash($ctx, $hash) {
$fdlObj = $this->modx->getObject('FDL', array(
'ctx' => $ctx,
'hash' => $hash
));
if (!$fdlObj) {
return FALSE;
}
return TRUE;
}
/**
* Get applied plugins and set custom properties by event's provider
* @param type $eventName
* @param type $customProperties
* @param type $toString
* @return type
*/
public function getPlugins($eventName, $customProperties = array(), $toString = false) {
if (empty($this->plugins)) {
return;
}
if (!is_array($customProperties))
$customProperties = array();
$this->plugins->setProperties($customProperties);
return $this->plugins->getPlugins($eventName, $toString);
}
}