Česky
Kamil Dudka

Web components

File detail

Name:DownloadFileBrowser.class.php [Download]
Location: src > lib
Size:23.3 KB
Last modification:2008-03-08 10:29

Source code

<?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');
  }
};
?>