<?php
/**
* @file FileBrowser.class.php
* Definition of FileBrowser class.
* @author Kamil Dudka <xdudka00@gmail.com>
* @ingroup FileBrowser
*/
/**
* @defgroup FileBrowser
* At http://dudka.cz/FileBrowser may be available abstract, documentation and tutorial for this component.
* @n Used @b Smarty @b templates: FileBrowser.tpl, FileBrowserData.tpl, FileBrowserDispatch.tpl
* @n Used @b CSS @b IDs: content, fbImgPreview, fbWorkspace
* @n Used @b CSS @b classes: backLink, fileDetail, head, name, SourceCode, FileContent, imgPreview, FileBrowser, FileBrowserPath, Download, odd, even, treeImg, size, date, time, actions, defaultAction
* @n Used @b images: tree_minus.png, tree_plus.png, tree_item.png, alfa_pixel.gif, download.png, download_dir.png, back.png
*/
// Import classes
ClassFactory::importClass('PageServer');
ClassFactory::importClass('FbScanDir');
ClassFactory::importClass('FbFileType');
/**
* Server part of FileBrowser component.
* @ingroup FileBrowser
*/
class FileBrowser {
/**
* Check if FileBrowser is requested by client.
* @return Return true if FileBrowser is requested.
*/
public static function requested() {
return @$_GET['show']=='FileBrowser';
}
/**
* @attention This static method must be always called before any session_start() or header() command.
* This method handles FileBrowser standalone requests if needed.
*/
public static function handleRequest() {
if (!self::requested())
return false;
// Direct access to files placed in special folder ($page/html by default)
if (!isset(self::$instance_) && @$_GET['action']=='directAccess') {
self::$instance_ = new FileBrowser(Config::instance()->htmlDir());
self::instance()->actionDirectAccess();
return true;
}
// Execute PHP script placed in special folder ($page/php by default)
if (!isset(self::$instance_) && @$_GET['action']=='execPHP') {
self::$instance_ = new FileBrowser(Config::instance()->execPhpDir());
self::instance()->actionExecPhp();
return true;
}
// Create FileBrowser instance
$obj = self::instance();
switch (@$_GET['action']) {
case 'download':
$obj->actionDownload();
return true;
case 'downloadUTF8':
$obj->actionDownloadUTF8();
return true;
case 'thumbnail640':
$obj->actionThumbnail(640);
return true;
case 'thumbnail320':
$obj->actionThumbnail(320);
return true;
case 'thumbnail160':
$obj->actionThumbnail(160);
return true;
case 'fullImage':
$obj->actionFullImage();
return true;
case 'sendPdf':
$obj->actionSendPdf();
return true;
case 'asyncDispatch':
$obj->asyncDispatch();
return true;
case 'browse':
default:
return false;
}
}
/**
* Title of managed conent (file/directory).
* @return Return string with page title @b postfix or empty string if FileBrowser is not active.
*/
public static function titlePostfix() {
if (!self::requested())
return '';
else
return ' - '.self::instance()->contentTitle();
}
/**
* This static method should be called inside page body (inside content page block).
* Handle non-standalone FileBrowser request.
* @param page Name of page maintained by PageServer.
* @param pageTitle Visible page title.
*/
public static function showContent($page, $pageTitle) {
$obj = self::instance();
$obj->page_ = $page;
$obj->pageTitle_ = $pageTitle;
if ($obj->isDir())
$obj->viewFileBrowser();
else
$obj->viewFileDetail();
}
/**
* This static method should be called inside page head section.
* Insert links to Javascript files and inline scripts to page head.
*/
public static function genHeadIfNeeded() {
if (!self::requested())
return false;
$obj = self::instance();
if ($obj->isDir())
$obj->genHead();
return true;
}
private static $instance_;
/**
* Access point to singleton instance.
*/
private static function instance() {
if (!isset(self::$instance_))
self::$instance_ = new FileBrowser;
return self::$instance_;
}
// Local data
private $l10n_;
private $page_;
private $pageTitle_;
private $smarty_;
// Read-only properties (private for now)
private $dirDocRoot_; private function dirDocRoot() { return $this->dirDocRoot_; }
private $dirWebRoot_; private function dirWebRoot() { return $this->dirWebRoot_; }
private $path_; private function path() { return $this->path_; }
private $fullPath_; private function fullPath() { return $this->fullPath_; }
/**
* @param branch Select where to find managed files. Config option filesDir used by default.
* @throw ExceptionNotFound Exception is thrown when request is not valid.
*/
private function __construct($branch=null) {
$config = Config::instance();
$docRoot = $config->documentRoot();
$webRoot = $config->webRoot();
if (!isset($branch))
$branch = $config->filesDir();
$page = @$_GET['page'];
if (!PageServer::isPageValid($page))
throw new ExceptionNotFound;
$this->dirDocRoot_ = realpath($docRoot.$branch.'/'.$page);
if (false===$this->dirDocRoot_)
throw new ExceptionNotFound($docRoot.$branch.'/'.$page);
if (!is_dir($this->dirDocRoot_))
// Files root directory is not directory
throw new ExceptionNotFound($this->dirDocRoot_);
$this->dirWebRoot_ = $webRoot.'/'.$page.$branch;
$path = @$_GET['path'];
$this->fullPath_ = realpath($this->dirDocRoot_.'/'.$path);
if (false===$this->fullPath_)
// Relative path is not valid
throw new ExceptionNotFound($this->dirDocRoot_.'/'.$path);
if (!file_exists($this->fullPath_))
// Another check (needed for hosting on Blueboard)
throw new ExceptionNotFound($this->fullPath_);
if (0!==strpos($this->fullPath_, $this->dirDocRoot_))
// Possible attack
throw new ExceptionNotFound(realpath($this->fullPath_));
// FIXME: Not tested yet
$path = str_replace($this->dirDocRoot_, '', $this->fullPath_);
$path = ereg_replace('^\/*', '', $path);
$path = ereg_replace('\/*$', '', $path);
$this->path_ = $path;
// FileBrowser Localization
// TODO: Define l10n outside FileBrowser?
$this->l10n_ = new L10n;
$this->l10n_->setTranslation('cz', 'Ascending', 'Vzestupně');
$this->l10n_->setTranslation('cz', 'Back', 'Zpět');
$this->l10n_->setTranslation('cz', 'BackToDirectory', 'Přejít do složky');
$this->l10n_->setTranslation('en', 'BackToDirectory', 'Go to directory');
$this->l10n_->setTranslation('cz', 'BackToMainPage', 'Zpět na hlavní stránku');
$this->l10n_->setTranslation('en', 'BackToMainPage', 'Back to main page');
$this->l10n_->setTranslation('cz', 'Descending', 'Sestupně');
$this->l10n_->setTranslation('en', 'DetectedCharset', 'Detected charset');
$this->l10n_->setTranslation('cz', 'DetectedCharset', 'Detekovaná znaková sada');
$this->l10n_->setTranslation('cz', 'Download', 'Stáhnout');
$this->l10n_->setTranslation('en', 'DownloadAsUTF8', 'Download as UTF-8');
$this->l10n_->setTranslation('cz', 'DownloadAsUTF8', 'Stáhnout jako UTF-8');
$this->l10n_->setTranslation('cz', 'Directory', 'Adresář');
$this->l10n_->setTranslation('cz', 'File', 'Soubor');
$this->l10n_->setTranslation('en', 'FileBrowser', 'File browser');
$this->l10n_->setTranslation('cz', 'FileBrowser', 'Prohlížeč souborů');
$this->l10n_->setTranslation('en', 'FileContent', 'File content');
$this->l10n_->setTranslation('cz', 'FileContent', 'Obsah souboru');
$this->l10n_->setTranslation('en', 'FileDetail', 'File detail');
$this->l10n_->setTranslation('cz', 'FileDetail', 'Detail souboru');
$this->l10n_->setTranslation('cz', 'Full size', 'Plná velikost');
$this->l10n_->setTranslation('cz', 'Location', 'Umístění');
$this->l10n_->setTranslation('en', 'Mtime', 'Last modification');
$this->l10n_->setTranslation('cz', 'Mtime', 'Poslední změna');
$this->l10n_->setTranslation('cz', 'Name', 'Jméno');
$this->l10n_->setTranslation('cz', 'Open', 'Otevřít');
$this->l10n_->setTranslation('cz', 'Preview', 'Náhled');
$this->l10n_->setTranslation('cz', 'Size', 'Velikost');
$this->l10n_->setTranslation('en', 'SourceCode', 'Source code');
$this->l10n_->setTranslation('cz', 'SourceCode', 'Zdrojový kód');
$this->l10n_->setTranslation('cz', 'View', 'Zobrazit');
}
/**
* @return Return title for managed content (file/directory)
*/
private function contentTitle() {
$contentType = $this->isDir()?'Directory':'File';
return $this->l10n_->tr($contentType).': /'.$this->path_;
}
/**
* @return Return true if target is directory.
*/
private function isDir() {
return is_dir($this->fullPath_);
}
/**
* Initialize internal smarty instance.
*/
private function initSmarty() {
$this->smarty_ = SmartyFactory::createSmarty();
$this->smarty_->assign('TR', $this->l10n_->trMap());
$this->smarty_->assign('page', $this->page_);
$this->smarty_->assign('pageTitle', $this->pageTitle_);
$this->smarty_->assign('imgCollapsed', 'tree_plus.png');
$this->smarty_->assign('rowClasses', Array('odd', 'even'));
}
/**
* Initialize location bar. Useful only in non-standalone mode.
*/
private function initLocationBar() {
$fullPath = $this->dirDocRoot().'/'.$this->path();
$dir = realpath($fullPath);
if (false===$dir)
throw new ExceptionNotFound($fullPath);
if (!is_dir($dir))
$dir = dirname($dir);
// Parse path from end and find directories (in reverse order)
$pathStack = Array();
while ($this->dirDocRoot() != $dir) {
$lastDir = $dir;
$dir = realpath($dir.'/..');
$href = str_replace($this->dirDocRoot(), $this->dirWebRoot(), $lastDir);
$text = str_replace($dir.'/', '', $lastDir);
$pathStack []= Array(
'href' => $href,
'text' => $text);
}
// Add link to root
$pathStack []= Array(
'href' => $this->dirWebRoot().'/',
'text' => $this->page_);
// Reverse array and put to local Smarty instance
$nav = array_reverse($pathStack);
$this->smarty_->assign('locationBar', $nav);
}
/**
* Build file list and put list to internal smarty instance.
* @param bLinkToParrent Set true to generate link '[..]' as first link in non-root directory.
*/
private function buildFileList($bLinkToParent) {
$path = $this->path_;
// Get file list and sort
$scanDir = new FbScanDir($this->fullPath_);
$scanDir->setLocale($this->l10n_);
$this->smarty_->assign('sort', $scanDir->sortLinkArray());
$dirList = $scanDir->dirList();
$fileList = $scanDir->fileList();
if ($bLinkToParent && realpath($this->dirDocRoot_)!=realpath($this->fullPath_))
// Add link to parent
array_unshift($dirList, Array('name'=>'..'));
// Directory-only decorations
foreach ($dirList as &$current) {
$name = $current['name'];
$current['name'] = Array('text'=>$name);
$fileLink = $this->dirWebRoot_.'/'.$path.(empty($path)?'':'/').$name;
$current['name']['href']= $fileLink;
if ('..'==$name) {
$current['name']['text'] = '[..]';
continue;
}
$current['treeImg'] = Array(
'src' => 'tree_plus.png',
'alt' => '+',
'class' => 'imgCollapsed');
$current['actions'] = Array(
Array(
'href' => $fileLink.'?action=download',
'text' => self::instance()->l10n_->tr('Download'),
'img' => 'download_dir.png',
'default' => false),
// Array(
// 'href' => $fileLink,
// 'text' => self::instance()->l10n_->tr('Open'),
// 'default' => true),
);
}
// File-only decorations
foreach ($fileList as &$current) {
$name = $current['name'];
$ft = new FbFileType($this->dirDocRoot_.'/'.$path.'/'.$name);
$current['name'] = Array('text'=>$name);
$fileLink = $this->dirWebRoot_.'/'.$path.(empty($path)?'':'/').$name;
$current['name']['href']= $fileLink;
if ($ft->isDownloadDefault())
$current['name']['href'] .= '?action=download';
else if ($ft->isPdf())
$current['name']['href'] .= '?action=sendPdf';
$current['size'] = FbScanDir::sizeToString($current['size']);
$current['actions'] = Array(
Array(
'href' => $fileLink.'?action=download',
'text' => self::instance()->l10n_->tr('Download'),
'img' => 'download.png',
'default' => $ft->isDownloadDefault()));
if ($ft->isTextFile() || $ft->isPdf())
$current['actions'] []= Array(
'href' => $current['name']['href'],
'text' => self::instance()->l10n_->tr('View'),
'default' => true);
if ($ft->isImage()) {
$current['actions'] []= Array(
'href' => $fileLink,
'text' => self::instance()->l10n_->tr('Preview'),
'default' => true);
$current['actions'] []= Array(
'href' => 'javascript:void window.open(\''.$fileLink.'?action=fullImage\');',
'text' => self::instance()->l10n_->tr('Full size'),
'default' => false);
}
$current['treeImg'] = Array(
'src' => 'tree_item.png',
'alt' => '');
}
// Build common (dir/file)list
$all = array_merge($dirList, $fileList);
// Common (dir/file) decorations
foreach ($all as &$current) {
if (array_key_exists('mtime', $current)) {
$current['date'] = FbScanDir::mtimeDateToSTring($current['mtime']);
$current['time'] = FbScanDir::mtimeTimeToSTring($current['mtime']);
}
}
$this->smarty_->assign('fileList', $all);
}
/**
* Generate file browser in non-standalone mode and send direct to output.
*/
private function viewFileBrowser() {
$this->initSmarty();
$this->initLocationBar();
$this->buildFileList(true);
$this->smarty_->display('FileBrowser.tpl');
}
/**
* Generate file detail in non-standalone mode and send direct to output.
*/
private function viewFileDetail() {
// Initialize smarty
$this->initSmarty();
$this->initLocationBar();
// Get information
$name = basename($this->fullPath_);
$size = filesize($this->fullPath_);
$mtime = filemtime($this->fullPath_);
$dirLink = str_replace($this->dirDocRoot_, $this->dirWebRoot_, dirname($this->fullPath_));
// Set smarty variables
$fileDetail = Array(
'name' => $name,
'dirLink' => $dirLink,
'size' => FbScanDir::sizeToString($size),
'date' => FbScanDir::mtimeDateToString($mtime),
'time' => FbScanDir::mtimeTimeToString($mtime));
$this->smarty_->assign('fileDetail', $fileDetail);
// Guess file type
$ft = new FbFileType($this->fullPath_);
if ($ft->canHighLight())
// Show source code using GeSHI
$this->smarty_->assign('fileSourceCode', $ft->highlight());
else if ($ft->isTextFile())
// Show file's text content
$this->smarty_->assign('fileTextContent', $ft->getText());
else if ($ft->isImage())
$this->smarty_->assign('isImage', true);
// Show detected charset
if ($ft->isTextFile()) {
$charset = $ft->getDetectedCharset();
if (false!=$charset)
$this->smarty_->assign('detectedCharset', $charset);
}
// Render smarty template
$this->smarty_->display('FileBrowser.tpl');
}
/**
* Send file (or directory) to client and force download at client's side.
*/
private function actionDownload() {
if ($this->isDir())
return $this->downloadDir();
header('Content-Description: File Transfer');
header('Content-Type: application/force-download');
header('Content-Disposition: attachment; filename="'.basename($this->fullPath_).'"');
header('Content-Length: ' . filesize($this->fullPath_));
readfile ($this->fullPath_);
exit();
}
/**
* Send text file in UTF-8 to client and force download at client's side.
*/
private function actionDownloadUTF8() {
$ft = new FbFileType($this->fullPath_);
if (!$ft->isTextFile())
throw new ExceptionNotFound($this->fullPath_);
header('Content-Description: File Transfer');
header('Content-Type: application/force-download');
header('Content-Disposition: attachment; filename="'.basename($this->fullPath_).'"');
$text = $ft->getText();
header('Content-Length: ' . strlen($ttext));
echo $text;
exit();
}
/**
* Build ZIP archive from an directory and send to client.
*/
private function downloadDir() {
// Create temporary file
$config = Config::instance();
$tmpDir = $config->documentRoot().$config->tmpDir();
$tmpFile = tempnam($tmpDir, 'FB_ZIP_');
// Create and initialize ZIP archive
$zip = new ZipArchive;
$zip->open($tmpFile, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE);
$fullPath = $this->fullPath_.'/';
$clientName = basename(ereg_replace('\/$', '', $this->fullPath_));
$zip->addEmptyDir($clientName);
// Walk directory recursively and add files/dirs to archive
$dirStack = Array('');
while (0!=count($dirStack)) {
$currentDir = array_pop($dirStack);
$scanList = scandir($fullPath.$currentDir, 1);
array_pop($scanList);
array_pop($scanList);
foreach ($scanList as $currentFile) {
if (is_dir($fullPath.$currentDir.$currentFile)) {
// directory
$zip->addEmptyDir($clientName.'/'.$currentDir.$currentFile);
array_push($dirStack, $currentDir.$currentFile.'/');
}
else
//file
$zip->addFile($fullPath.$currentDir.$currentFile, $clientName.'/'.$currentDir.$currentFile);
}
}
$zip->close();
// Send archive to client
header('Content-Description: File Transfer');
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="'.$clientName.'.zip"');
header('Content-Length: ' . filesize($tmpFile));
readfile ($tmpFile);
// Clean up and exit
unlink($tmpFile);
exit();
}
/**
* Generate image thumbnail and send to client as image.
* @param maxSize Maximum width or height (the greater one) of the thumbnail.
*/
private function actionThumbnail($maxSize) {
$fileName = $this->fullPath_;
$imgName = basename($fileName);
$sizeStruct = @getimagesize($fileName);
// Guess image type
$img = null;
if (eregi('\.png$', $imgName))
$img = @imagecreatefrompng($fileName);
else if (eregi('\.jpe?g$', $imgName))
$img = @imagecreatefromjpeg($fileName);
else if (eregi('\.gif', $imgName))
$img = @imagecreatefromgif($fileName);
if (!isset($img))
throw new ExceptionNotFound($fileName);
// Check size
$max = max($sizeStruct[0], $sizeStruct[1]);
$ratio = 1;
if ($max > $maxSize) {
$ratio = (float)$maxSize/$max;
$width = $ratio * $sizeStruct[0];
$height = $ratio * $sizeStruct[1];
// Create image with desired dimensions
$imgDest = imagecreatetruecolor($width, $height);
// Smooth scale of image
imagecopyresampled(
$imgDest, $img, 0, 0, 0, 0,
$width, $height, $sizeStruct[0], $sizeStruct[1]);
imagedestroy($img);
// PNG thumbnail was too big, moved to jpeg
/*header('Content-type: image/png');
imagepng($imgDest, null, 9);*/
header('Content-type: image/jpeg');
imageinterlace($imgDest, 1);
imagejpeg($imgDest, null, 90);
imagedestroy($imgDest);
} else {
@imagedestroy($img);
$this->actionFullImage();
}
exit(0);
}
/**
* Send file to client as image.
* Currently png, jpg, jpeg and gif extensions are supported.
*/
private function actionFullImage() {
$fileName = $this->fullPath_;
$imgName = basename($fileName);
// Guess image type
$img = null;
if (eregi('\.png$', $imgName))
header('Content-type: image/png');
else if (eregi('\.jpe?g$', $imgName))
header('Content-type: image/jpeg');
else if (eregi('\.gif$', $imgName))
header('Content-type: image/gif');
else if (ereg('\.svg$', $imgName))
header('Content-type: image/svg+xml');
else
throw new ExceptionNotFound($fileName);
header('Content-length: '.filesize($fileName));
readfile($fileName);
exit();
}
/**
* Send file to client with MIME application/pdf
*/
private function actionSendPdf() {
$fileName = $this->fullPath_;
if (!eregi('\.pdf$', $fileName))
throw new ExceptionNotFound($fileName);
header('Content-type: application/pdf');
header('Content-length: '.filesize($fileName));
readfile($fileName);
exit();
}
/**
* Direct access to files placed in special folder ($page/html by default)
*/
private function actionDirectAccess() {
$fileName = $this->fullPath();
if (is_dir($fileName))
throw new ExceptionNotFound($fileName);
// TODO: Send MIME
if (ereg('\.css$', $fileName))
// Needed for FF!
header('Content-type: text/css');
header('Content-length: '.filesize($fileName));
readfile($fileName);
exit();
}
/**
* Execute PHP script placed in special folder ($page/php by default)
*/
private function actionExecPhp() {
$fileName = $this->fullPath();
if (is_dir($fileName))
throw new ExceptionNotFound($fileName);
if (ereg('\.php$', $fileName)) {
// Script execution
chdir(dirname($fileName));
require($fileName);
} else {
// Send non-script file direct
$this->actionDirectAccess();
}
exit();
}
/**
* Send asynchronous dispatch as response to FileBrowser.class.js request.
*/
private function asyncDispatch() {
if (!$this->isDir())
// Unexpected navigation to file??
throw new ExceptionNotFound;
// Build file list
$this->initSmarty();
$this->buildFileList(false);
// Content-type respose header is needed for Konqueror and FF
// if using XMLHttp::resposeXML property, Opera does not need this
Header('Content-type: application/xhtml+xml');
$this->smarty_->display('FileBrowserDispatch.tpl');
exit();
}
/**
* This method is called inside page head section.
* Insert links to Javascript files and inline scripts to page head.
*/
private function genHead() {
$config = Config::instance();
$webRoot = $config->webRoot();
$fbScript = $webRoot.$config->scriptDir().'/FileBrowser.class.js';
$imgRoot = $webRoot.$config->imgDir();
echo ' <script type="text/javascript" src="'.$fbScript.'"></script>'."\n";
?> <script type="text/javascript">
function fbOnload() {
(new Image).src='<?php echo $imgRoot.'/alfa_pixel.gif' ?>';
fb = new FileBrowser;
fb.imgExpanded = '<?php echo $imgRoot.'/tree_minus.png' ?>';
fb.imgCollapsed = '<?php echo $imgRoot.'/tree_plus.png' ?>';
fb.imgNoChildern = '<?php echo $imgRoot.'/tree_item.png' ?>';
fb.imgAlfaPixel = '<?php echo $imgRoot.'/alfa_pixel.gif' ?>';
fb.init();
}
</script>
<?php
JSOnloadList::instance()->addFunction('fbOnload');
}
};
?>