source: branches/MootoolsFileManager-Update/plugins/MootoolsFileManager/mootools-filemanager/Assets/Connector/FileManager.php @ 1300

Last change on this file since 1300 was 1300, checked in by gogo, 9 years ago

Update the MootoolsFileManager? to the latest cpojer with some modifications.
Add a demo for the MFM examples/mootools-file-manager.php
Change the default config for ImageManager? and ExtendedFileManager? for added security.

File size: 69.3 KB
Line 
1<?php
2/*
3 * Script: FileManager.php
4 *   MooTools FileManager - Backend for the FileManager Script
5 *
6 * Authors:
7 *  - Christoph Pojer (http://cpojer.net) (author)
8 *  - James Ehly (http://www.devtrench.com)
9 *  - Fabian Vogelsteller (http://frozeman.de)
10 *
11 * License:
12 *   MIT-style license.
13 *
14 * Copyright:
15 *   Copyright (c) 2009 [Christoph Pojer](http://cpojer.net)
16 *
17 * Dependencies:
18 *   - Upload.php
19 *   - Image.class.php
20 *   - getId3 Library
21 *
22 * Options:
23 *   - directory: (string) The base directory to be used for the FileManager
24 *   - assetBasePath: (string, optional) The path to all images and swf files used by the filemanager
25 *   - thumbnailPath: (string) The path where the thumbnails of the pictures will be saved
26 *   - mimeTypesPath: (string, optional) The path to the MimeTypes.ini file.
27 *   - dateFormat: (string, defaults to *j M Y - H:i*) The format in which dates should be displayed
28 *   - maxUploadSize: (integer, defaults to *20280000* bytes) The maximum file size for upload in bytes
29 *   - maxImageSize: (integer, default is 1024) The maximum number of pixels in both height and width an image can have, if the user enables "resize on upload"
30 *   - upload: (boolean, defaults to *true*) allow uploads, this is also set in the FileManager.js (this here is only for security protection when uploads should be deactivated)
31 *   - destroy: (boolean, defaults to *true*) allow files to get deleted, this is also set in the FileManager.js (this here is only for security protection when file/directory delete operations should be deactivated)
32 *   - create: (boolean, defaults to *true*) allow creating new subdirectories, this is also set in the FileManager.js (this here is only for security protection when dir creates should be deactivated)
33 *   - move: (boolean, defaults to *true*) allow file and directory move/rename and copy, this is also set in the FileManager.js (this here is only for security protection when rename/move/copy should be deactivated)
34 *   - download: (boolean, defaults to *true*) allow downloads, this is also set in the FileManager.js (this here is only for security protection when downloads should be deactivated)
35 *   - allowExtChange: (boolean, defaults to *false*) allow the file extension to be changed when performing a rename operation.
36 *   - safe: (boolean, defaults to *true*) If true, disallows 'exe', 'dll', 'php', 'php3', 'php4', 'php5', 'phps' and saves them as 'txt' instead.
37 *   - chmod: (integer, default is 0777) the permissions set to the uploaded files and created thumbnails (must have a leading "0", e.g. 0777)
38 *   - UploadIsAuthorized_cb (function/reference, default is *null*) authentication + authorization callback which can be used to determine whether the given file may be uploaded.
39 *     The parameter $action = 'upload'.
40 *   - DownloadIsAuthorized_cb (function/reference, default is *null*) authentication + authorization callback which can be used to determine whether the given file may be downloaded.
41 *     The parameter $action = 'download'.
42 *   - CreateIsAuthorized_cb (function/reference, default is *null*) authentication + authorization callback which can be used to determine whether the given subdirectory may be created.
43 *     The parameter $action = 'create'.
44 *   - DestroyIsAuthorized_cb (function/reference, default is *null*) authentication + authorization callback which can be used to determine whether the given file / subdirectory tree may be deleted.
45 *     The parameter $action = 'destroy'.
46 *   - MoveIsAuthorized_cb (function/reference, default is *null*) authentication + authorization callback which can be used to determine whether the given file / subdirectory may be renamed, moved or copied.
47 *     Note that currently support for copying subdirectories is missing.
48 *     The parameter $action = 'move'.
49 *
50 * For all authorization hooks (callback functions) the following applies:
51 *
52 *     The callback should return TRUE for yes (permission granted), FALSE for no (permission denied).
53 *     Parameters sent to the callback are:
54 *       ($this, $action, $fileinfo)
55 *     where $fileinfo is an array containing info about the file being uploaded, $action is a (string) identifying the current operation, $this is a reference to this FileManager instance.
56 *     $action was included as a redundant parameter to each callback as a simple means to allow users to hook a single callback function to all the authorization hooks, without the need to create a wrapper function for each.
57 *
58 *     For more info about the hook parameter $fileinfo contents and a basic implementation, see Demos/manager.php and Demos/selectImage.php
59 *
60 * Notes on relative paths and safety / security:
61 *
62 *   If any option is specifying a relative path, e.g. '../Assets' or 'Media/Stuff/', this is assumed to be relative to the request URI path,
63 *   i.e. dirname($_SERVER['SCRIPT_NAME']).
64 *
65 *   Requests may post/submit relative paths as arguments to their FileManager events/actions in $_GET/$_POST, and those relative paths will be
66 *   regarded as relative to the request URI handling script path, i.e. dirname($_SERVER['SCRIPT_NAME']) to make the most
67 *   sense from bother server and client coding perspective.
68 *
69 *
70 *   We also assume that any of the paths may be specified from the outside, so each path is processed and filtered to prevent malicious intent
71 *   from succeeding. (An example of such would be an attacker posting his own 'destroy' event request requesting the destruction of
72 *   '../../../../../../../../../etc/passwd' for example. In more complex rigs, the attack may be assisted through attacks at these options' paths,
73 *   so these are subjected to the same scrutiny in here.)
74 *
75 *   All paths, absolute or relative, as passed to the event handlers (see the onXXX methods of this class) are ENFORCED TO ABIDE THE RULE
76 *   'every path resides within the BASEDIR rooted tree' without exception.
77 *   When paths apparently don't, they are forcibly coerced into adherence to this rule. Because we can do without exceptions to important rules. ;-)
78 *
79 *   BASEDIR equals the path pointed at by the options['directory'] setting. It is therefore imperative that you ensure this value is
80 *   correctly set up; worst case, this setting will equal DocumentRoot.
81 *   In other words: you'll never be able to reach any file or directory outside this site's DocumentRoot directory tree, ever.
82 *
83 *
84 *   When you need your paths to be restricted to the bounds of the options['directory'] tree (which is a subtree of the DocumentRoot based
85 *   tree), you may wish to use the CheckFile(), getPath() and getDir() methods instead of getRealPath() and getRealDir(), as the latter
86 *   restrict targets to within the DocumentRoot tree only.
87 *
88 *   getPath() and getRealPath() both deliver absolute paths relative to DocumentRoot, hence suitable for use in URIs and feeding to client side
89 *   scripts, while getRealDir() and getDir() both return absolute paths in the server filesystem perspective, i.e. the latter are suitable for
90 *   server side script based file operation functions.
91 */
92
93// ----------- compatibility checks ----------------------------------------------------------------------------
94if (version_compare(PHP_VERSION, '5.2.0') < 0)
95{
96    // die horribly: server does not match our requirements!
97    header('HTTP/1.0 500 FileManager requires PHP 5.2.0 or later', true, 500); // Internal server error
98    throw Exception('FileManager requires PHP 5.2.0 or later');   // this exception will most probably not be caught; that's our intent!
99}
100
101if (function_exists('UploadIsAuthenticated'))
102{
103    // die horribly: user has not upgraded his callback hook(s)!
104    header('HTTP/1.0 500 FileManager callback has not been upgraded!', true, 500); // Internal server error
105    throw Exception('FileManager callback has not been upgraded!');   // this exception will most probably not be caught; that's our intent!
106}
107
108//-------------------------------------------------------------------------------------------------------------
109
110
111if (!defined('MTFM_PATH'))
112{
113    $base = str_replace('\\','/',dirname(__FILE__));
114    define('MTFM_PATH', $base);
115}
116
117require_once(MTFM_PATH . '/Upload.php');
118require_once(MTFM_PATH . '/Image.class.php');
119
120class FileManager
121{
122  protected $path = null;
123  protected $basedir = null;                    // absolute path equivalent, filesystem-wise, for options['directory']
124  protected $options;
125  protected $post;
126  protected $get;
127
128  public function __construct($options)
129  {
130    $this->options = array_merge(array(
131      /*
132       * Note that all default paths as listed below are transformed to DocumentRoot-based paths
133       * through the getRealPath() invocations further below:
134       */
135      'directory' => MTFM_PATH . '/Files/',
136      'assetBasePath' => MTFM_PATH . '/../../Assets/',
137      'thumbnailPath' => MTFM_PATH . '/../../Assets/Thumbs/',  // written like this so we're completely clear on where the default thumbnails directory will be
138      'mimeTypesPath' => MTFM_PATH . '/MimeTypes.ini',
139      'dateFormat' => 'j M Y - H:i',
140      'maxUploadSize' => 2600 * 2600 * 3,
141      'maxImageSize' => 999999, // Xinha: We have separate X/Y dimension in 'suggestedMaxImageDimension', don't want this
142      'upload' => false,
143      'destroy' => false,
144      'create' => false,
145      'move' => false,
146      'download' => false,
147      /* ^^^ this last one is easily circumnavigated if it's about images: when you can view 'em, you can 'download' them anyway.
148       *     However, for other mime types which are not previewable / viewable 'in their full bluntal nugity' ;-) , this will
149       *     be a strong deterent.
150       *
151       *     Think Springer Verlag and PDFs, for instance. You can have 'em, but only /after/ you've ...
152       */
153      'allowExtChange' => false,
154      'safe' => true,
155      'chmod' => 0777,
156      'UploadIsAuthorized_cb' => null,
157      'DownloadIsAuthorized_cb' => null,
158      'CreateIsAuthorized_cb' => null,
159      'DestroyIsAuthorized_cb' => null,
160      'MoveIsAuthorized_cb' => null,
161     
162     // Xinha: Allow to specify the "Resize Large Images" tolerance level.
163     'suggestedMaxImageDimension' => array('width' => 1024, 'height' => 768),
164    ), (is_array($options) ? $options : array()));
165
166    $this->options['thumbnailPath'] = FileManagerUtility::getRealPath($this->options['thumbnailPath'], $this->options['chmod'], true); // create path if nonexistent
167    $this->options['assetBasePath'] = FileManagerUtility::getRealPath($this->options['assetBasePath']);
168    $this->options['mimeTypesPath'] = FileManagerUtility::getRealDir($this->options['mimeTypesPath'], 0, false, false); // filespec, not a dirspec!
169    $this->options['directory'] = FileManagerUtility::getRealPath($this->options['directory']);
170    $this->basedir = FileManagerUtility::getSiteRoot() . $this->options['directory'];
171
172    header('Expires: Fri, 01 Jan 1990 00:00:00 GMT');
173    header('Cache-Control: no-cache, no-store, max-age=0, must-revalidate');
174
175    $this->get = $_GET;
176    $this->post = $_POST;
177  }
178
179  public function fireEvent($event)
180  {
181    $event = $event ? 'on' . ucfirst($event) : null;
182    if (!$event || !method_exists($this, $event)) $event = 'onView';
183
184    $this->{$event}();
185  }
186
187  /**
188   * @return array the FileManager options and settings.
189   */
190  public function getSettings()
191  {
192    return array_merge(array(
193        'basedir' => $this->basedir
194    ), $this->options);
195  }
196
197  private function _onView($dir, $json, $mime_filter, $list_type)
198  {
199    $files = ($files = glob($dir . '*')) ? $files : array();
200
201    $root = FileManagerUtility::getSiteRoot();
202
203    if ($dir != $this->basedir) array_unshift($files, $dir . '..');
204    natcasesort($files);
205    foreach ($files as $file)
206    {
207      $file = self::normalize($file);
208      $url = str_replace($root,'',$file);
209
210      $mime = $this->getMimeType($file);
211      if ($mime_filter && $mime != 'text/directory' && !FileManagerUtility::startsWith($mime, $mime_filter))
212        continue;
213
214      /*
215       * each image we inspect may throw an exception due to a out of memory warning
216       * (which is far better than without those: a silent fatal abort!)
217       *
218       * However, now that we do have a way to check most memory failures occurring in here (due to large images
219       * and too little available RAM) we /still/ want a directory view; we just want to skip/ignore/mark those
220       * overly large ones.
221       */
222      $thumb = false;
223      try
224      {
225        // access the image and create a thumbnail image; this can fail dramatically
226        if(strpos($mime,'image') !== false)
227          $thumb = $this->getThumb($file);
228      }
229      catch (Exception $e)
230      {
231         // do nothing, except mark image as 'not suitable for thumbnailing'
232      }
233
234      $icon = ($list_type == 'thumb' && $thumb)
235        ? $this->options['thumbnailPath'] . $thumb
236        : $this->getIcon($file, $list_type != 'thumb'); // TODO: add extra icons for those bad format and superlarge images with make us b0rk?
237
238      // list files, except the thumbnail folder itself or any file in it:
239      if(!FileManagerUtility::startswith($url, substr($this->options['thumbnailPath'],0,-1)))
240      {
241        $out[is_dir($file) ? 0 : 1][] = array(
242          'path' => FileManagerUtility::rawurlencode_path($url),
243          'name' => pathinfo($file, PATHINFO_BASENAME),
244          'date' => date($this->options['dateFormat'], @filemtime($file)),
245          'mime' => $mime,
246          'thumbnail' => FileManagerUtility::rawurlencode_path($icon),
247          'icon' => FileManagerUtility::rawurlencode_path($this->getIcon($file,true)),
248          'size' => @filesize($file)
249        );
250      }
251    }
252    return array_merge((is_array($json) ? $json : array()), array(
253        //'assetBasePath' => $this->options['assetBasePath'],
254        //'thumbnailPath' => $this->options['thumbnailPath'],
255        //'ia_directory' => $this->options['directory'],
256        //'ia_dir' => $dir,
257        //'ia_root' => $root,
258        //'ia_basedir' => $this->basedir,
259        'root' => substr($this->options['directory'], 1),
260        'path' => str_replace($this->basedir,'',$dir),               // is relative to 'root'
261        'dir' => array(
262            'name' => pathinfo($dir, PATHINFO_BASENAME),
263            'date' => date($this->options['dateFormat'], @filemtime($dir)),
264            'mime' => 'text/directory',
265            'thumbnail' => $this->getIcon($dir),
266            'icon' => $this->getIcon($dir,true)
267          ),
268      'files' => array_merge(!empty($out[0]) ? $out[0] : array(), !empty($out[1]) ? $out[1] : array())
269    ));
270  }
271
272  /**
273   * Process the 'view' event (default event fired by fireEvent() method)
274   *
275   * Returns a JSON encoded directory view list.
276   *
277   * Expected parameters:
278   *
279   * $_POST['directory']     path relative to basedir a.k.a. options['directory'] root
280   *
281   * $_POST['filter']        optional mimetype filter string, amy be the part up to and
282   *                         including the slash '/' or the full mimetype. Only files
283   *                         matching this (set of) mimetypes will be listed.
284   *                         Examples: 'image/' or 'application/zip'
285   *
286   * $_POST['type']          'thumb' will produce a list view including thumbnail and other
287   *                         information with each listed file; other values will produce
288   *                         a basic list view (similar to Windows Explorer 'list' view).
289   *
290   * Errors will produce a JSON encoded error report, including at least two fields:
291   *
292   * status                  0 for error; nonzero for success
293   *
294   * error                   error message
295   *
296   * Next to these, the JSON encoded output will, with high probability, include a
297   * list view of the parent or 'basedir' as a fast and easy fallback mechanism for client side
298   * viewing code. However, severe and repetitive errors may not produce this
299   * 'fallback view list' so proper client code should check the 'status' field in the
300   * JSON output.
301   */
302  protected function onView()
303  {
304    // try to produce the view; if it b0rks, retry with the parent, until we've arrived at the basedir:
305    // then we fail more severely.
306
307    $mime_filter = null;
308    $list_type = null;
309    $emsg = null;
310    $jserr = array(
311            'status' => 1
312        );
313    $bottomdir = $this->basedir;
314
315    try
316    {
317        $mime_filter = ((isset($_POST['filter']) && !empty($_POST['filter'])) ? $_POST['filter'].'/' : null);
318        $list_type = ((isset($_POST['type']) && $_POST['type'] == 'list') ? 'list' : 'thumb');
319
320        $dir = $this->getDir(!empty($this->post['directory']) ? $this->post['directory'] : null);
321    }
322    catch(FileManagerException $e)
323    {
324        $emsg = $e->getMessage();
325        $dir = $this->basedir;
326    }
327    catch(Exception $e)
328    {
329        // catching other severe failures; since this can be anything it may not be a translation keyword in the message...
330        $emsg = $e->getMessage();
331        $dir = $this->basedir;
332    }
333
334    // loop until we drop below the bottomdir; meanwhile getDir() above guarantees that $dir is a subdir of bottomdir, hence dir >= bottomdir.
335    do
336    {
337        try
338        {
339            $rv = $this->_onView($dir, $jserr, $mime_filter, $list_type);
340            echo json_encode($rv);
341            return;
342        }
343        catch(FileManagerException $e)
344        {
345            $emsg = $e->getMessage();
346        }
347        catch(Exception $e)
348        {
349            // catching other severe failures; since this can be anything it may not be a translation keyword in the message...
350            $emsg = $e->getMessage();
351        }
352
353        // only set up the new json error report array when this is the first exception we got:
354        if ($jserr['status'])
355        {
356            // check the error message and see if it is a translation code word (with or without parameters) or just a generic error report string
357            $e = explode(':', $emsg, 2);
358            if (preg_match('/[^A-Za-z0-9_-]/', $e[0]))
359            {
360                // generic message. ouch.
361                $jserr = array(
362                        'status' => 0,
363                        'error' => $emsg
364                    );
365            }
366            else
367            {
368                $jserr = array(
369                        'status' => 0,
370                        'error' => '${backend.' . $e[0] . '}' . (isset($e[1]) ? $e[1] : '')
371                    );
372            }
373        }
374
375        // step down to the parent dir and retry:
376        $dir = dirname($dir);
377        if (!FileManagerUtility::endsWith($dir, '/')) $dir .= '/';
378
379    } while (strcmp($dir, $bottomdir) >= 0);
380
381    // when we fail here, it's pretty darn bad and nothing to it.
382    // just push the error JSON as go.
383    echo json_encode($jserr);
384  }
385
386  /**
387   * Process the 'detail' event
388   *
389   * Returns a JSON encoded HTML chunk describing the specified file (metadata such
390   * as size, format and possibly a thumbnail image as well)
391   *
392   * Expected parameters:
393   *
394   * $_POST['directory']     path relative to basedir a.k.a. options['directory'] root
395   *
396   * $_POST['file']          filename (including extension, of course) of the file to
397   *                         be detailed.
398   *
399   * Errors will produce a JSON encoded error report, including at least two fields:
400   *
401   * status                  0 for error; nonzero for success
402   *
403   * error                   error message
404   */
405  protected function onDetail()
406  {
407  try
408  {
409    if (empty($this->post['file']))
410        throw new FileManagerException('nofile');
411
412    $url = $this->getPath(!empty($this->post['directory']) ? $this->post['directory'] : null);
413    $dir = FileManagerUtility::getSiteRoot() . $url;
414    $file = pathinfo($this->post['file'], PATHINFO_BASENAME);
415
416    $dir .= $file;
417    $url .= $file;
418
419    if (!$this->checkFile($dir))
420        throw new FileManagerException('nofile');
421
422    // spare the '/' dir separators from URL encoding:
423    $encoded_url = FileManagerUtility::rawurlencode_path($url);
424
425    $mime = $this->getMimeType($dir);
426    $content = null;
427   
428    // Xinha: We want to get some more information about what has been selected in a way
429    // we can use it.  Effectively what gets put in here will be passed into the
430    // 'onDetails' event handler of your FileManager object (if any).
431    $extra_return_detail = array
432      (
433        'url'  => $url,
434        'mime' => $mime
435      );
436   
437    // image
438    if (FileManagerUtility::startsWith($mime, 'image/'))
439    {
440      // generates a random number to put on the end of the image, to prevent caching
441      $randomImage = '?'.md5(uniqid(rand(),1));
442      $size = @getimagesize($dir);
443      // check for badly formatted image files (corruption); we'll handle the overly large ones next
444      if (!$size)
445        throw new FileManagerException('corrupt_img:' . $url);
446   
447        // Xinha: Return some information about the image which can be access
448        // from the onDetails event handler in FileManager
449       $extra_return_detail['width']  = $size[0];
450       $extra_return_detail['height'] = $size[1];       
451       
452      $thumbfile = $this->options['thumbnailPath'] . $this->getThumb($dir);
453      $content = '<dl>
454          <dt>${width}</dt><dd>' . $size[0] . 'px</dd>
455          <dt>${height}</dt><dd>' . $size[1] . 'px</dd>
456        </dl>
457        <h2>${preview}</h2>
458        ';
459      try
460      {
461          $tnc = '<a href="'.$encoded_url.'" data-milkbox="preview" title="'.htmlentities($file, ENT_QUOTES, 'UTF-8').'"><img src="' . FileManagerUtility::rawurlencode_path($thumbfile) . $randomImage . '" class="preview" alt="preview" /></a>';
462      }
463      catch (Exception $e)
464      {
465          $tnc = '<a href="'.$encoded_url.'" data-milkbox="preview" title="'.htmlentities($file, ENT_QUOTES, 'UTF-8').'"><img src="' . FileManagerUtility::rawurlencode_path($this->getIcon($dir)).$randomImage . '" class="preview" alt="preview" /></a>';
466      }
467      $content .= $tnc;
468    // text preview
469    }
470    elseif (FileManagerUtility::startsWith($mime, 'text/') || $mime == 'application/x-javascript')
471    {
472      $filecontent = file_get_contents($dir, false, null, 0);
473      if (!FileManagerUtility::isBinary($filecontent))
474      {
475        $content = '<div class="textpreview"><pre>' . str_replace(array('$', "\t"), array('&#36;', '&nbsp;&nbsp;'), htmlentities($filecontent,ENT_QUOTES,'UTF-8')) . '</pre></div>';
476      }
477      // else: fall back to 'no preview available'
478    // zip
479    }
480    elseif ($mime == 'application/zip')
481    {
482      require_once(MTFM_PATH . '/Assets/getid3/getid3.php');
483
484      $out = array(array(), array());
485      $getid3 = new getID3();
486      $getid3->Analyze($dir);
487      foreach ($getid3->info['zip']['files'] as $name => $size)
488      {
489        $isdir = is_array($size) ? true : false;
490        $out[($isdir) ? 0 : 1][$name] = '<li><a><img src="'.FileManagerUtility::rawurlencode_path($this->getIcon($dir,true)).'" alt="" /> ' . $name . '</a></li>';
491      }
492      natcasesort($out[0]);
493      natcasesort($out[1]);
494      $content = '<ul>' . implode(array_merge($out[0], $out[1])) . '</ul>';
495    // swf
496    }
497    elseif ($mime == 'application/x-shockwave-flash')
498    {
499      require_once(MTFM_PATH . '/Assets/getid3/getid3.php');
500      $getid3 = new getID3();
501      $getid3->Analyze($dir);
502
503      $content = '<dl>
504          <dt>${width}</dt><dd>' . $getid3->info['swf']['header']['frame_width']/10 . 'px</dd>
505          <dt>${height}</dt><dd>' . $getid3->info['swf']['header']['frame_height']/10 . 'px</dd>
506          <dt>${length}</dt><dd>' . round(($getid3->info['swf']['header']['length']/$getid3->info['swf']['header']['frame_count'])) . 's</dd>
507        </dl>
508        <h2>${preview}</h2>
509        <div class="object">
510          <object type="application/x-shockwave-flash" data="'.FileManagerUtility::rawurlencode_path($url).'" width="500" height="400">
511            <param name="scale" value="noscale" />
512            <param name="movie" value="'.FileManagerUtility::rawurlencode_path($url).'" />
513          </object>
514        </div>';
515    // audio
516    }
517    elseif (FileManagerUtility::startsWith($mime, 'audio/'))
518    {
519      require_once(MTFM_PATH . '/Assets/getid3/getid3.php');
520      $getid3 = new getID3();
521      $getid3->Analyze($dir);
522      getid3_lib::CopyTagsToComments($getid3->info);
523
524      $dewplayer = FileManagerUtility::rawurlencode_path($this->options['assetBasePath'] . 'dewplayer.swf');
525      $content = '<dl>
526          <dt>${title}</dt><dd>' . $getid3->info['comments']['title'][0] . '</dd>
527          <dt>${artist}</dt><dd>' . $getid3->info['comments']['artist'][0] . '</dd>
528          <dt>${album}</dt><dd>' . $getid3->info['comments']['album'][0] . '</dd>
529          <dt>${length}</dt><dd>' . $getid3->info['playtime_string'] . '</dd>
530          <dt>${bitrate}</dt><dd>' . round($getid3->info['bitrate']/1000) . 'kbps</dd>
531        </dl>
532        <h2>${preview}</h2>
533        <div class="object">
534          <object type="application/x-shockwave-flash" data="' . $dewplayer . '" width="200" height="20" id="dewplayer" name="dewplayer">
535            <param name="wmode" value="transparent" />
536            <param name="movie" value="' . $dewplayer . '" />
537            <param name="flashvars" value="mp3=' . FileManagerUtility::rawurlencode_path($url) . '&amp;volume=50&amp;showtime=1" />
538          </object>
539        </div>';
540    }
541    // else: fall back to 'no preview available'
542
543    echo json_encode(array_merge(array(
544      'status' => 1,
545      'content' => $content ? $content : '<div class="margin">
546        ${nopreview}
547      </div>'                 //<br/><button value="' . $url . '">${download}</button>
548    ), $extra_return_detail));
549    }
550    catch(FileManagerException $e)
551    {
552        $emsg = explode(':', $e->getMessage(), 2);
553        echo json_encode(array(
554                'status' => 0,
555                'content' => '<div class="margin">
556                  ${nopreview}
557                  <div class="failure_notice">
558                    <h3>${error}</h3>
559                    <p>mem usage: ' . number_format(memory_get_usage() / 1E6, 2) . ' MB : ' . number_format(memory_get_peak_usage() / 1E6, 2) . ' MB</p>
560                    <p>${backend.' . $emsg[0] . '}' . (isset($emsg[1]) ? $emsg[1] : '') . '</p>
561                  </div>
562                </div>'       // <br/><button value="' . $url . '">${download}</button>
563            ));
564    }
565    catch(Exception $e)
566    {
567        // catching other severe failures; since this can be anything and should only happen in the direst of circumstances, we don't bother translating
568        echo json_encode(array(
569                'status' => 0,
570                'content' => '<div class="margin">
571                  ${nopreview}
572                  <div class="failure_notice">
573                    <h3>${error}</h3>
574                    <p>mem usage: ' . number_format(memory_get_usage() / 1E6, 2) . ' MB : ' . number_format(memory_get_peak_usage() / 1E6, 2) . ' MB</p>
575                    <p>' . $e->getMessage() . '</p>
576                  </div>
577                </div>'       // <br/><button value="' . $url . '">${download}</button>
578            ));
579    }
580  }
581
582  /**
583   * Process the 'destroy' event
584   *
585   * Delete the specified file or directory and return a JSON encoded status of success
586   * or failure.
587   *
588   * Note that when images are deleted, so are their thumbnails.
589   *
590   * Expected parameters:
591   *
592   * $_POST['directory']     path relative to basedir a.k.a. options['directory'] root
593   *
594   * $_POST['file']          filename (including extension, of course) of the file to
595   *                         be detailed.
596   *
597   * Errors will produce a JSON encoded error report, including at least two fields:
598   *
599   * status                  0 for error; nonzero for success
600   *
601   * error                   error message
602   */
603  protected function onDestroy()
604  {
605    try
606    {
607        if (!$this->options['destroy'])
608            throw new FileManagerException('disabled');
609        if (empty($this->post['file']))
610            throw new FileManagerException('nofile');
611
612        $dir = $this->getDir(!empty($this->post['directory']) ? $this->post['directory'] : null);
613        $file = pathinfo($this->post['file'], PATHINFO_BASENAME);
614
615        $fileinfo = array(
616            'dir' => $dir,
617            'file' => $file
618        );
619
620        if (!$this->checkFile($dir . $file))
621            throw new FileManagerException('nofile');
622
623        if (!empty($this->options['DestroyIsAuthorized_cb']) && function_exists($this->options['DestroyIsAuthorized_cb']) && !$this->options['DestroyIsAuthorized_cb']($this, 'destroy', $fileinfo))
624            throw new FileManagerException('authorized');
625
626        if (!$this->unlink($dir . $file))
627            throw new FileManagerException('unlink_failed:' . $dir . $file);
628
629        echo json_encode(array(
630          'status' => 1,
631          'content' => 'destroyed'
632        ));
633    }
634    catch(FileManagerException $e)
635    {
636        $emsg = explode(':', $e->getMessage(), 2);
637        echo json_encode(array(
638                'status' => 0,
639                'error' => '${backend.' . $emsg[0] . '}' . (isset($emsg[1]) ? $emsg[1] : '')
640            ));
641    }
642    catch(Exception $e)
643    {
644        // catching other severe failures; since this can be anything and should only happen in the direst of circumstances, we don't bother translating
645        echo json_encode(array(
646                'status' => 0,
647                'error' => $e->getMessage()
648            ));
649    }
650  }
651
652  /**
653   * Process the 'create' event
654   *
655   * Create the specified subdirectory and give it the configured permissions
656   * (options['chmod'], default 0777) and return a JSON encoded status of success
657   * or failure.
658   *
659   * Expected parameters:
660   *
661   * $_POST['directory']     path relative to basedir a.k.a. options['directory'] root
662   *
663   * $_POST['file']          name of the subdirectory to be created
664   *
665   * Extra input parameters considered while producing the JSON encoded directory view.
666   * This may not seem relevant for an empty directory, but these parameters are also
667   * considered when providing the fallback directory view in case an error occurred
668   * and then the listed directory (either the parent or the basedir itself) may very
669   * likely not be empty!
670   *
671   * $_POST['filter']        optional mimetype filter string, amy be the part up to and
672   *                         including the slash '/' or the full mimetype. Only files
673   *                         matching this (set of) mimetypes will be listed.
674   *                         Examples: 'image/' or 'application/zip'
675   *
676   * $_POST['type']          'thumb' will produce a list view including thumbnail and other
677   *                         information with each listed file; other values will produce
678   *                         a basic list view (similar to Windows Explorer 'list' view).
679   *
680   * Errors will produce a JSON encoded error report, including at least two fields:
681   *
682   * status                  0 for error; nonzero for success
683   *
684   * error                   error message
685   */
686  protected function onCreate()
687  {
688    try
689    {
690        $mime_filter = ((isset($_POST['filter']) && !empty($_POST['filter'])) ? $_POST['filter'].'/' : null);
691        $list_type = ((isset($_POST['type']) && $_POST['type'] == 'list') ? 'list' : 'thumb');
692
693        if (!$this->options['create'])
694            throw new FileManagerException('disabled');
695        if (empty($this->post['file']))
696            throw new FileManagerException('nofile');
697
698        $dir = $this->getDir(!empty($this->post['directory']) ? $this->post['directory'] : null);
699        $file = $this->getName(array('filename' => $this->post['file']), $dir);  // a directory has no 'extension'!
700        if (!$file)
701            throw new FileManagerException('nofile');
702
703        $fileinfo = array(
704            'dir' => $dir,
705            'file' => $file,
706            'chmod' => $this->options['chmod']
707        );
708        if (!empty($this->options['CreateIsAuthorized_cb']) && function_exists($this->options['CreateIsAuthorized_cb']) && !$this->options['CreateIsAuthorized_cb']($this, 'create', $fileinfo))
709            throw new FileManagerException('authorized');
710
711        if (!@mkdir($file, $fileinfo['chmod']))
712            throw new FileManagerException('mkdir_failed:' . $file);
713
714        // success, now show the new directory as a list view:
715        $jsok = array(
716                'status' => 1
717            );
718        $rv = $this->_onView($file . '/', $jsok, $mime_filter, $list_type);
719        echo json_encode($rv);
720    }
721    catch(FileManagerException $e)
722    {
723        $emsg = explode(':', $e->getMessage(), 2);
724        $jserr = array(
725                'status' => 0,
726                'error' => '${backend.' . $emsg[0] . '}' . (isset($emsg[1]) ? $emsg[1] : '')
727            );
728        // and fall back to showing the PARENT directory
729        try
730        {
731            $rv = $this->_onView($dir, $jserr, $mime_filter, $list_type);
732            echo json_encode($rv);
733        }
734        catch (Exception $e)
735        {
736            // and fall back to showing the BASEDIR directory
737            try
738            {
739                $dir = $this->getDir();
740                $rv = $this->_onView($dir, $jserr, $mime_filter, $list_type);
741                echo json_encode($rv);
742            }
743            catch (Exception $e)
744            {
745                // when we fail here, it's pretty darn bad and nothing to it.
746                // just push the error JSON as go.
747                echo json_encode($jserr);
748            }
749        }
750    }
751    catch(Exception $e)
752    {
753        // catching other severe failures; since this can be anything and should only happen in the direst of circumstances, we don't bother translating
754        $jserr = array(
755                'status' => 0,
756                'error' => $e->getMessage()
757            );
758        // and fall back to showing the PARENT directory
759        try
760        {
761            $rv = $this->_onView($dir, $jserr, $mime_filter, $list_type);
762            echo json_encode($rv);
763        }
764        catch (Exception $e)
765        {
766            // and fall back to showing the BASEDIR directory
767            try
768            {
769                $dir = $this->getDir();
770                $rv = $this->_onView($dir, $jserr, $mime_filter, $list_type);
771                echo json_encode($rv);
772            }
773            catch (Exception $e)
774            {
775                // when we fail here, it's pretty darn bad and nothing to it.
776                // just push the error JSON as go.
777                echo json_encode($jserr);
778            }
779        }
780    }
781  }
782
783  /**
784   * Process the 'download' event
785   *
786   * Send the file content of the specified file for download by the client.
787   * Only files residing within the directory tree rooted by the
788   * 'basedir' (options['directory']) will be allowed to be downloaded.
789   *
790   * Expected parameters:
791   *
792   * $_GET['file']          filepath of the file to be downloaded
793   *
794   * On errors a HTTP 403 error response will be sent instead.
795   */
796  protected function onDownload()
797  {
798    try
799    {
800        if (!$this->options['download'])
801            throw new FileManagerException('disabled');
802        if (empty($_GET['file']))
803            throw new FileManagerException('nofile');
804        // no need to check explicitly for '../' and './' here as getDir() will take care of it all!
805
806        // change the path to fit your websites document structure
807        $path = $this->getDir($_GET['file'], 0, false, false);
808        if (!is_file($path))
809            throw new FileManagerException('nofile');
810
811        $fileinfo = array(
812            'file' => $path
813        );
814        if (!empty($this->options['DownloadIsAuthorized_cb']) && function_exists($this->options['DownloadIsAuthorized_cb']) && !$this->options['DownloadIsAuthorized_cb']($this, 'download', $fileinfo))
815            throw new FileManagerException('authorized');
816
817        if ($fd = fopen($path, "r"))
818        {
819            $fsize = filesize($path);
820            $path_parts = pathinfo($path);
821            $ext = strtolower($path_parts["extension"]);
822            switch ($ext)
823            {
824            case "pdf":
825                header('Content-type: application/pdf');
826                header('Content-Disposition: attachment; filename="' . $path_parts["basename"] . '"'); // use 'attachment' to force a download
827                break;
828
829             // add here more headers for diff. extensions
830
831            default;
832                header('Content-type: application/octet-stream');
833                header('Content-Disposition: filename="' . $path_parts["basename"] . '"');
834            }
835            header("Content-length: $fsize");
836            header("Cache-control: private"); //use this to open files directly
837
838            fpassthru($fd);
839            fclose($fd);
840        }
841    }
842    catch(FileManagerException $e)
843    {
844        // we don't care whether it's a 404, a 403 or something else entirely: we feed 'em a 403 and that's final!
845        if (function_exists('send_response_status_header'))
846        {
847            send_response_status_header(403);
848            echo $e->getMessage();
849        }
850        else
851        {
852            // no smarties detection whether we're running on fcgi or bare iron, we assume the latter:
853            header('HTTP/1.0 403 Forbidden', true, 403);
854            echo $e->getMessage();
855        }
856    }
857    catch(Exception $e)
858    {
859        // we don't care whether it's a 404, a 403 or something else entirely: we feed 'em a 403 and that's final!
860        if (function_exists('send_response_status_header'))
861        {
862            send_response_status_header(403);
863            echo $e->getMessage();
864        }
865        else
866        {
867            // no smarties detection whether we're running on fcgi or bare iron, we assume the latter:
868            header('HTTP/1.0 403 Forbidden', true, 403);
869            echo $e->getMessage();
870        }
871    }
872  }
873
874  /**
875   * Process the 'upload' event
876   *
877   * Process and store the uploaded file in the designated location.
878   * Images will be resized when possible and applicable. A thumbnail image will also
879   * be preproduced when possible.
880   * Return a JSON encoded status of success or failure.
881   *
882   * Expected parameters:
883   *
884   * $_GET['directory']     path relative to basedir a.k.a. options['directory'] root
885   *
886   * $_FILES[]              the metadata for the uploaded file
887   *
888   * Errors will produce a JSON encoded error report, including at least two fields:
889   *
890   * status                  0 for error; nonzero for success
891   *
892   * error                   error message
893   */
894  protected function onUpload()
895  {
896    try
897    {
898      if (!$this->options['upload'])
899        throw new FileManagerException('disabled');
900      if (!Upload::exists('Filedata'))
901        throw new FileManagerException('nofile');
902
903      $dir = $this->getDir(!empty($this->get['directory']) ? $this->get['directory'] : null);
904      $file = $this->getName($_FILES['Filedata']['name'], $dir);
905      if (!$file)
906        throw new FileManagerException('nofile');
907      $fi = pathinfo($file);
908      if (!$fi['filename'])
909        throw new FileManagerException('nofile');
910
911      /*
912      Security:
913
914      Upload::move() processes the unfiltered version of $_FILES[]['name'], at least to get the extension,
915      unless we ALWAYS override the filename and extension in the options array below. That's why we
916      calculate the extension at all times here.
917      */
918      if (!is_string($fi['extension']) || strlen($fi['extension']) == 0) // can't use 'empty()' as "0" is a valid extension itself.
919      {
920        //enforce a mandatory extension, even when there isn't one (due to filtering or original input producing none)
921        $fi['extension'] = 'txt';
922      }
923      else if ($this->options['safe'] && in_array(strtolower($fi['extension']), array('exe', 'dll', 'com', 'php', 'php3', 'php4', 'php5', 'phps')))
924      {
925        $fi['extension'] = 'txt';
926      }
927
928      $fileinfo = array(
929        'dir' => $dir,
930        'name' => $fi['filename'],
931        'extension' => $fi['extension'],
932        'size' => $_FILES['Filedata']['size'],
933        'maxsize' => $this->options['maxUploadSize'],
934        'mimes' => $this->getAllowedMimeTypes(),
935        'ext2mime_map' => $this->getMimeTypeDefinitions(),
936        'chmod' => $this->options['chmod'] & 0666   // security: never make those files 'executable'!
937      );
938      if (!empty($this->options['UploadIsAuthorized_cb']) && function_exists($this->options['UploadIsAuthorized_cb']) && !$this->options['UploadIsAuthorized_cb']($this, 'upload', $fileinfo))
939        throw new FileManagerException('authorized');
940
941      $file = Upload::move('Filedata', $dir, $fileinfo);
942      $file = self::normalize($file);
943
944      /*
945       * NOTE: you /can/ (and should be able to, IMHO) upload 'overly large' image files to your site, but the thumbnailing process step
946       *       happening here will fail; we have memory usage estimators in place to make the fatal crash a non-silent one, i,e, one
947       *       where we still have a very high probability of NOT fatally crashing the PHP iunterpreter but catching a suitable exception
948       *       instead.
949       *       Having uploaded such huge images, a developer/somebody can always go in later and up the memory limit if the site admins
950       *       feel it is deserved. Until then, no thumbnails of such images (though you /should/ be able to milkbox-view the real thing!)
951       */
952      if (FileManagerUtility::startsWith($this->getMimeType($file), 'image/') && !empty($this->get['resize']))
953      {
954        $img = new Image($file);
955        $size = $img->getSize();
956        // Xinha: We have separate width/height max dimensions, if these fail, the fallback of the
957        //   maxImageSize is used, which we set to some very high number by default for back compat
958        if ($size['width'] > $this->options['suggestedMaxImageDimension']['width'])
959          $img->resize( $this->options['suggestedMaxImageDimension']['width'])->save();
960        elseif ($size['height'] > $this->options['suggestedMaxImageDimension']['height'])
961          $img->resize(null, $this->options['suggestedMaxImageDimension']['height'])->save();       
962        else
963        // Image::resize() takes care to maintain the proper aspect ratio, so this is easy:
964        if ($size['width'] > $this->options['maxImageSize'] || $size['height'] > $this->options['maxImageSize'])
965          $img->resize($this->options['maxImageSize'], $this->options['maxImageSize'])->save();
966        unset($img);
967      }
968
969      echo json_encode(array(
970        'status' => 1,
971        'name' => pathinfo($file, PATHINFO_BASENAME)
972      ));
973    }
974    catch(UploadException $e)
975    {
976      echo json_encode(array(
977        'status' => 0,
978        'error' => class_exists('ValidatorException') ? strip_tags($e->getMessage()) : '${backend.' . $e->getMessage() . '}' // This is for Styx :)
979      ));
980    }
981    catch(FileManagerException $e)
982    {
983        $emsg = explode(':', $e->getMessage(), 2);
984        echo json_encode(array(
985                'status' => 0,
986                'error' => '${backend.' . $emsg[0] . '}' . (isset($emsg[1]) ? $emsg[1] : '')
987            ));
988    }
989    catch(Exception $e)
990    {
991      // catching other severe failures; since this can be anything and should only happen in the direst of circumstances, we don't bother translating
992      echo json_encode(array(
993        'status' => 0,
994        'error' => $e->getMessage()
995      ));
996    }
997  }
998
999  /**
1000   * Process the 'move' event (with is used by both move/copy and rename client side actions)
1001   *
1002   * Copy or move/rename a given file or directory and return a JSON encoded status of success
1003   * or failure.
1004   *
1005   * Expected parameters:
1006   *
1007   * $_POST['copy']            nonzero value means copy, zero or nil for move/rename
1008   *
1009   * Source filespec:
1010   *
1011   *   $_POST['directory']     path relative to basedir a.k.a. options['directory'] root
1012   *
1013   *   $_POST['file']          original name of the file/subdirectory to be renamed/copied
1014   *
1015   * Destination filespec:
1016   *
1017   *   $_POST['newDirectory']  path relative to basedir a.k.a. options['directory'] root;
1018   *                           target directory where the file must be moved / copied
1019   *
1020   *   $_POST['name']          target name of the file/subdirectory to be renamed
1021   *
1022   * Errors will produce a JSON encoded error report, including at least two fields:
1023   *
1024   * status                    0 for error; nonzero for success
1025   *
1026   * error                     error message
1027   */
1028  protected function onMove()
1029  {
1030    try
1031    {
1032        if (!$this->options['move'])
1033            throw new FileManagerException('disabled');
1034        if (empty($this->post['file']))
1035            throw new FileManagerException('nofile');
1036
1037        $rename = empty($this->post['newDirectory']) && !empty($this->post['name']);
1038        $is_copy = (!empty($this->post['copy'])  && $this->post['copy']);
1039        $dir = $this->getDir(!empty($this->post['directory']) ? $this->post['directory'] : null);
1040        $file = pathinfo($this->post['file'], PATHINFO_BASENAME);
1041
1042        $is_dir = is_dir($dir . $file);
1043
1044        // note: we do not support copying entire directories, though directory rename/move is okay
1045        if (!$this->checkFile($dir . $file) || ($is_copy && $is_dir))
1046            throw new FileManagerException('nofile');
1047
1048        if($rename)
1049        {
1050            $fn = 'rename';
1051            $newdir = null;
1052            if ($is_dir)
1053                $newname = $this->getName(array('filename' => $this->post['name']), $dir);  // a directory has no 'extension'
1054            else
1055                $newname = $this->getName($this->post['name'], $dir);
1056
1057            // when the new name seems to have a different extension, make sure the extension doesn't change after all:
1058            // Note: - if it's only 'case' we're changing here, then exchange the extension instead of appending it.
1059            //       - directories do not have extensions
1060            $extOld = pathinfo($file, PATHINFO_EXTENSION);
1061            $extNew = pathinfo($newname, PATHINFO_EXTENSION);
1062            if ((!$this->options['allowExtChange'] || (!$is_dir && empty($extNew))) && !empty($extOld) && strtolower($extOld) != strtolower($extNew))
1063            {
1064                $newname .= '.' . $extOld;
1065            }
1066        }
1067        else
1068        {
1069            $fn = ($is_copy ? 'copy' : 'rename' /* 'move' */);
1070            $newdir = $this->getDir(!empty($this->post['newDirectory']) ? $this->post['newDirectory'] : null);
1071            $newname = $this->getName($file, $newdir);
1072        }
1073
1074        if (!$newname)
1075            throw new FileManagerException('nonewfile');
1076
1077        $fileinfo = array(
1078            'dir' => $dir,
1079            'file' => $file,
1080            'newdir' => $newdir,
1081            'newname' => $newname,
1082            'rename' => $rename,
1083            'is_dir' => $is_dir,
1084            'function' => $fn
1085        );
1086
1087        if (!empty($this->options['MoveIsAuthorized_cb']) && function_exists($this->options['MoveIsAuthorized_cb']) && !$this->options['MoveIsAuthorized_cb']($this, 'move', $fileinfo))
1088            throw new FileManagerException('authorized');
1089
1090        if($rename)
1091        {
1092            // try to remove the thumbnail related to the original file; don't mind if it doesn't exist
1093            if(!$is_dir)
1094            {
1095                if (!$this->deleteThumb($dir . $file))
1096                    throw new FileManagerException('delete_thumbnail_failed');
1097            }
1098        }
1099
1100        if (!@$fn($dir . $file, $newname))
1101            throw new FileManagerException($fn . '_failed:' . $dir . $file . ':' . $newname);
1102
1103        echo json_encode(array(
1104            'status' => 1,
1105            'name' => pathinfo($newname, PATHINFO_BASENAME)
1106        ));
1107    }
1108    catch(FileManagerException $e)
1109    {
1110        $emsg = explode(':', $e->getMessage(), 2);
1111        echo json_encode(array(
1112                'status' => 0,
1113                'error' => '${backend.' . $emsg[0] . '}' . (isset($emsg[1]) ? $emsg[1] : '')
1114            ));
1115    }
1116    catch(Exception $e)
1117    {
1118        // catching other severe failures; since this can be anything and should only happen in the direst of circumstances, we don't bother translating
1119        echo json_encode(array(
1120                'status' => 0,
1121                'error' => $e->getMessage()
1122            ));
1123    }
1124  }
1125
1126
1127
1128  /**
1129   * Delete a file or directory, inclusing subdirectories and files.
1130   *
1131   * Return TRUE on success, FALSE when an error occurred.
1132   *
1133   * Note that the routine will try to percevere and keep deleting other subdirectories
1134   * and files, even when an error occurred for one or more of the subitems: this is
1135   * a best effort policy.
1136   */
1137  protected function unlink($file)
1138  {
1139    if (!$file || !FileManagerUtility::startsWith($file, $this->basedir))
1140        return false;
1141
1142    $rv = true;
1143    if(is_dir($file))
1144    {
1145      $files = glob($file . '/*');
1146      if (is_array($files))
1147        foreach ($files as $f)
1148        {
1149          $rv &= $this->unlink($f);
1150          $rv &= $this->deleteThumb($f);
1151        }
1152
1153      $rv &= @rmdir($file);
1154    }
1155    else
1156    {
1157      if (file_exists($file))
1158      {
1159        $rv &= @unlink($file);
1160        $rv &= $this->deleteThumb($file);
1161      }
1162    }
1163    return $rv;
1164  }
1165
1166  /**
1167   * Make a cleaned-up, unique filename
1168   *
1169   * Return the file (dir + name + ext), or a unique, yet non-existing, variant thereof, where the filename
1170   * is appended with a '_' and a number, e.g. '_1', when the file itself already exists in the given
1171   * directory. The directory part of the returned value equals $dir.
1172   *
1173   * Return NULL when $file is empty or when the specified directory does not reside within the
1174   * directory tree rooted by options['directory']
1175   *
1176   * Note that the given filename will be converted to a legal filename, containing a filesystem-legal
1177   * subset of ASCII characters only, before being used and returned by this function.
1178   *
1179   * @param mixed $fileinfo     either a string containing a filename+ext or an array as produced by pathinfo().
1180   * @daram string $dir         path pointing at where the given file may exist.
1181   *
1182   * @return a filepath consisting of $dir and the cleaned up and possibly sequenced filename and file extension
1183   *         as provided by $fileinfo.
1184   */
1185  protected function getName($fileinfo, $dir)
1186  {
1187    if (!FileManagerUtility::endsWith($dir, '/')) $dir .= '/';
1188
1189    if (is_string($fileinfo))
1190    {
1191        $fileinfo = pathinfo($fileinfo);
1192    }
1193
1194    if (!is_array($fileinfo) || !$fileinfo['filename'] || !FileManagerUtility::startsWith($dir, $this->basedir)) return null;
1195
1196
1197    /*
1198     * since 'pagetitle()' is used to produce a unique, non-existing filename, we can forego the dirscan
1199     * and simply check whether the constructed filename/path exists or not and bump the suffix number
1200     * by 1 until it does not, thus quickly producing a unique filename.
1201     *
1202     * This is faster than using a dirscan to collect a set of existing filenames and feeding them as
1203     * an option array to pagetitle(), particularly for large directories.
1204     */
1205    $filename = FileManagerUtility::pagetitle($fileinfo['filename'], null, '-_., []()~!@+' /* . '#&' */, '-_,~@+#&');
1206    if (!$filename)
1207        return null;
1208
1209    // also clean up the extension: only allow alphanumerics in there!
1210    $ext = FileManagerUtility::pagetitle(!empty($fileinfo['extension']) ? $fileinfo['extension'] : null);
1211    $ext = (!empty($ext) ? '.' . $ext : null);
1212    // make sure the generated filename is SAFE:
1213    $file = $dir . $filename . $ext;
1214    if (file_exists($file))
1215    {
1216        /*
1217         * make a unique name. Do this by postfixing the filename with '_X' where X is a sequential number.
1218         *
1219         * Note that when the input name is already such a 'sequenced' name, the sequence number is
1220         * extracted from it and sequencing continues from there, hence input 'file_5' would, if it already
1221         * existed, thus be bumped up to become 'file_6' and so on, until a filename is found which
1222         * does not yet exist in the designated directory.
1223         */
1224        $i = 1;
1225        if (preg_match('/^(.*)_([1-9][0-9]*)$/', $filename, $matches))
1226        {
1227            $i = intval($matches[2]);
1228            if ('P'.$i != 'P'.$matches[2] || $i > 100000)
1229            {
1230                // very large number: not a sequence number!
1231                $i = 1;
1232            }
1233            else
1234            {
1235                $filename = $matches[1];
1236            }
1237        }
1238        do
1239        {
1240            $file = $dir . $filename . ($i ? '_' . $i : '') . $ext;
1241            $i++;
1242        } while (file_exists($file));
1243    }
1244
1245    // $file is now guaranteed to NOT exist
1246    return $file;
1247  }
1248
1249  protected function getIcon($file, $smallIcon = false)
1250  {
1251    if (FileManagerUtility::endsWith($file, '/..')) $ext = 'dir_up';
1252    elseif (is_dir($file)) $ext = 'dir';
1253    else $ext = pathinfo($file, PATHINFO_EXTENSION);
1254
1255    $largeDir = ($smallIcon === false ? 'Large/' : '');
1256    $path = (is_file(FileManagerUtility::getSiteRoot() . $this->options['assetBasePath'] . 'Images/Icons/' .$largeDir.$ext.'.png'))
1257      ? $this->options['assetBasePath'] . 'Images/Icons/'.$largeDir.$ext.'.png'
1258      : $this->options['assetBasePath'] . 'Images/Icons/'.$largeDir.'default.png';
1259
1260    return $path;
1261  }
1262
1263  protected function getThumb($file)
1264  {
1265    $thumb = $this->generateThumbName($file);
1266    $thumbPath = FileManagerUtility::getSiteRoot() . $this->options['thumbnailPath'] . $thumb;
1267    if (is_file($thumbPath))
1268      return $thumb;
1269    elseif(is_file(FileManagerUtility::getSiteRoot() . $this->options['thumbnailPath'] . basename($file)))
1270      return basename($file);
1271    else
1272      return $this->generateThumb($file,$thumbPath);
1273  }
1274
1275  protected function generateThumbName($file)
1276  {
1277    return 'thumb_'.md5($file).'_'.str_replace('.','_',basename($file)).'.png';
1278  }
1279
1280  protected function generateThumb($file,$thumbPath)
1281  {
1282    $img = new Image($file);
1283    $img->resize(250,250,true,false)->process('png',$thumbPath); // TODO: save as lossy / lower-Q jpeg to reduce filesize?
1284    unset($img);
1285    return basename($thumbPath);
1286  }
1287
1288  protected function deleteThumb($file)
1289  {
1290    $thumb = $this->generateThumbName($file);
1291    $thumbPath = FileManagerUtility::getSiteRoot() . $this->options['thumbnailPath'] . $thumb;
1292    if(is_file($thumbPath))
1293      return @unlink($thumbPath);
1294    return true;   // when thumbnail does not exist, say it is succesfully removed: all that counts is it doesn't exist anymore when we're done here.
1295  }
1296
1297  public function getMimeType($file)
1298  {
1299    return is_dir($file) ? 'text/directory' : Upload::mime($file, null, $this->getMimeTypeDefinitions());
1300  }
1301
1302  /**
1303   * Produce the absolute path equivalent, filesystem-wise, of the given $dir directory.
1304   *
1305   * The directory is enforced to sit within the directory tree rooted by options['directory']
1306   *
1307   * When the directory does not exist or does not match this restricting criterium, the
1308   * basedir path (abs path eqv. to options['directory']) is returned instead.
1309   *
1310   * In short: getDir() will guarantee the returned path equals the options['directory'] path or
1311   *           a subdirectory thereof. The returned path is an absolute path in the filesystem.
1312   */
1313  protected function getDir($dir = null, $chmod = 0777, $mkdir_if_notexist = false, $with_trailing_slash = true)
1314  {
1315    $dir = str_replace('\\','/', $dir);
1316    $basedir = $this->basedir;
1317    $root = FileManagerUtility::getSiteRoot();
1318    $dir = (!FileManagerUtility::startsWith($dir, '/') ? $basedir : $root) . $dir;
1319    $dir = FileManagerUtility::getRealDir($dir, $chmod, $mkdir_if_notexist, $with_trailing_slash);
1320    return $this->checkFile($mkdir_if_notexist ? dirname($dir) : $dir) ? $dir : $this->basedir;
1321  }
1322
1323  /**
1324   * Identical to getDir() apart from the fact that this method returns a DocumentRoot based abolute one.
1325   *
1326   * This function assumes the specified path is located within the options['directory'] a.k.a.
1327   * 'basedir' based directory tree.
1328   */
1329  protected function getPath($dir = null, $chmod = 0777, $mkdir_if_notexist = false, $with_trailing_slash = true)
1330  {
1331    $path = $this->getDir($dir, $chmod, $mkdir_if_notexist, $with_trailing_slash);
1332    $root = FileManagerUtility::getSiteRoot();
1333    $path = str_replace($root,'',$path);
1334
1335    return $path;
1336  }
1337
1338  /**
1339   * Determine whether the specified file or directory is not nil,
1340   * exists within the directory tree rooted by options['directory'] and
1341   * matches the permitted mimetypes restriction (optional $mime_filter)
1342   *
1343   * @return TRUE when all criteria are met, FALSE otherwise.
1344   */
1345  protected function checkFile($file, $mime_filter = null)
1346  {
1347    $mimes = $this->getAllowedMimeTypes($mime_filter);
1348
1349    $hasFilter = ($mime_filter && count($mimes));
1350    if ($hasFilter) array_push($mimes, 'text/directory');
1351    return !empty($file) && FileManagerUtility::startsWith($file, $this->basedir) && file_exists($file) && (!$hasFilter || in_array($this->getMimeType($file), $mimes));
1352  }
1353
1354  /**
1355   * Normalize a path by converting all slashes '/' and/or backslashes '\' and any mix thereof in the
1356   * specified path to UNIX/MAC/Win compatible single forward slashes '/'.
1357   */
1358  protected static function normalize($file)
1359  {
1360    return preg_replace('/(\\\|\/)+/', '/', $file);
1361  }
1362
1363  public function getAllowedMimeTypes($mime_filter = null)
1364  {
1365    $mimeTypes = array();
1366
1367    if (!$mime_filter) return null;
1368    if (!FileManagerUtility::endsWith($mime_filter, '/')) return array($mime_filter);
1369
1370    $mimes = $this->getMimeTypeDefinitions();
1371
1372    foreach ($mimes as $mime)
1373      if (FileManagerUtility::startsWith($mime, $mime_filter))
1374        $mimeTypes[] = $mime;
1375
1376    return $mimeTypes;
1377  }
1378
1379  public function getMimeTypeDefinitions()
1380  {
1381    static $mimes;
1382
1383    if (!$mimes) $mimes = parse_ini_file($this->options['mimeTypesPath']);
1384    if (!$mimes) $mimes = array(); // prevent faulty mimetype ini file from b0rking other code sections.
1385    return $mimes;
1386  }
1387}
1388
1389class FileManagerException extends Exception {}
1390
1391/* Stripped-down version of some Styx PHP Framework-Functionality bundled with this FileBrowser. Styx is located at: http://styx.og5.net */
1392class FileManagerUtility
1393{
1394  public static function endsWith($string, $look){
1395    return strrpos($string, $look)===strlen($string)-strlen($look);
1396  }
1397
1398  public static function startsWith($string, $look){
1399    return strpos($string, $look)===0;
1400  }
1401
1402  /**
1403   * Cleanup and check against 'already known names' in optional $options array.
1404   * Return a uniquified name equal to or derived from the original ($data).
1405   *
1406   * First clean up the given name ($data): by default all characters not part of the
1407   * set [A-Za-z0-9_] are converted to an underscore '_'; series of these underscores
1408   * are reduced to a single one, and characters in the set [_.,&+ ] are stripped from
1409   * the lead and tail of the given name, e.g. '__name' would therefor be reduced to
1410   * 'name'.
1411   *
1412   * Next, check the now cleaned-up name $data against an optional set of names ($options array)
1413   * and return the name itself when it does not exist in the set,
1414   * otherwise return an augmented name such that it does not exist in the set
1415   * while having been constructed as name plus '_' plus an integer number,
1416   * starting at 1.
1417   *
1418   * Example:
1419   * If the set is {'file', 'file_1', 'file_3'} then $data = 'file' will return
1420   * the string 'file_2' instead, while $data = 'fileX' will return that same
1421   * value: 'fileX'.
1422   *
1423   * @param string $data     the name to be cleaned and checked/uniquified
1424   * @param array $options   an optional array of strings to check the given name $data against
1425   * @param string $extra_allowed_chars     optional set of additional characters which should pass
1426   *                                        unaltered through the cleanup stage. a dash '-' can be
1427   *                                        used to denote a character range, while the literal
1428   *                                        dash '-' itself, when included, should be positioned
1429   *                                        at the very start or end of the string.
1430   *
1431   *                                        Note that ] must NOT need to be escaped; we do this
1432   *                                        ourselves.
1433   * @param string $trim_chars              optional set of additional characters which are trimmed off the
1434   *                                        start and end of the name ($data); note that de dash
1435   *                                        '-' is always treated as a literal dash here; no
1436   *                                        range feature!
1437   *                                        The basic set of characters trimmed off the name is
1438   *                                        [. ]; this set cannot be reduced, only extended.
1439   *
1440   * @return cleaned-up and uniquified name derived from ($data).
1441   */
1442  public static function pagetitle($data, $options = null, $extra_allowed_chars = null, $trim_chars = null)
1443  {
1444    static $regex;
1445    if (!$regex){
1446      $regex = array(
1447        explode(' ', 'Æ Ê Œ œ ß Ü ÃŒ Ö ö Ä À À Á Â Ã Ä Å &#260; &#258; Ç &#262; &#268; &#270; &#272; Ð È É Ê Ë &#280; &#282; &#286; Ì Í Î Ï &#304; &#321; &#317; &#313; Ñ &#323; &#327; Ò Ó Ô Õ Ö Ø &#336; &#340; &#344; Å  &#346; &#350; &#356; &#354; Ù Ú Û Ü &#366; &#368; Ý Åœ &#377; &#379; à á â ã À Ã¥ &#261; &#259; ç &#263; &#269; &#271; &#273; Ú é ê ë &#281; &#283; &#287; ì í î ï &#305; &#322; &#318; &#314; ñ &#324; &#328; ð ò ó ÃŽ õ ö Þ &#337; &#341; &#345; &#347; Å¡ &#351; &#357; &#355; ù ú û ÃŒ &#367; &#369; Ãœ ÿ ÅŸ &#378; &#380;'),
1448        explode(' ', 'Ae ae Oe oe ss Ue ue Oe oe Ae ae A A A A A A A A C C C D D D E E E E E E G I I I I I L L L N N N O O O O O O O R R S S S T T U U U U U U Y Z Z Z a a a a a a a a c c c d d e e e e e e g i i i i i l l l n n n o o o o o o o o r r s s s t t u u u u u u y y z z z'),
1449      );
1450    }
1451
1452    if (empty($data))
1453        return $data;
1454
1455    // fixup $extra_allowed_chars to ensure it's suitable as a character sequence for a set in a regex:
1456    //
1457    // Note:
1458    //   caller must ensure a dash '-', when to be treated as a separate character, is at the very end of the string
1459    if (is_string($extra_allowed_chars))
1460    {
1461        $extra_allowed_chars = str_replace(']', '\]', $extra_allowed_chars);
1462        if (strpos($extra_allowed_chars, '-') === 0)
1463        {
1464            $extra_allowed_chars = substr($extra_allowed_chars, 1) . (strpos($extra_allowed_chars, '-') != strlen($extra_allowed_chars) - 1 ? '-' : '');
1465        }
1466    }
1467    else
1468    {
1469        $extra_allowed_chars = '';
1470    }
1471    // accepts dots and several other characters, but do NOT tolerate dots or underscores at the start or end, i.e. no 'hidden file names' accepted, for example!
1472    $data = preg_replace('/[^A-Za-z0-9' . $extra_allowed_chars . ']+/', '_', str_replace($regex[0], $regex[1], $data));
1473    $data = trim($data, '_. ' . $trim_chars);
1474
1475    //$data = trim(substr(preg_replace('/(?:[^A-z0-9]|_|\^)+/i', '_', str_replace($regex[0], $regex[1], $data)), 0, 64), '_');
1476    return !empty($options) ? self::checkTitle($data, $options) : $data;
1477  }
1478
1479  protected static function checkTitle($data, $options = array(), $i = 0){
1480    if (!is_array($options)) return $data;
1481
1482    $lwr_data = strtolower($data);
1483
1484    foreach ($options as $content)
1485      if ($content && strtolower($content) == $lwr_data . ($i ? '_' . $i : ''))
1486        return self::checkTitle($data, $options, ++$i);
1487
1488    return $data.($i ? '_' . $i : '');
1489  }
1490
1491  public static function isBinary($str){
1492    $array = array(0, 255);
1493    for($i = 0; $i < strlen($str); $i++)
1494      if (in_array(ord($str[$i]), $array)) return true;
1495
1496    return false;
1497  }
1498
1499// unused method:
1500//
1501//  public static function getPath(){
1502//    static $path;
1503//    return $path ? $path : $path = pathinfo(str_replace('\\','/',__FILE__), PATHINFO_DIRNAME);
1504//  }
1505
1506  /**
1507   * Return the filesystem absolute path to the directory pointed at by this site's DocumentRoot.
1508   *
1509   * Note that the path is returned WITHOUT a trailing slash '/'.
1510   */
1511  public static function getSiteRoot()
1512  {
1513    $path = str_replace('\\','/',$_SERVER['DOCUMENT_ROOT']);
1514    $path = (FileManagerUtility::endsWith($path,'/')) ? substr($path, 0, -1) : $path;
1515
1516    return $path;
1517  }
1518
1519  /**
1520   * Return the filesystem absolute path to the directory pointed at by the current URI request.
1521   * For example, if the request was 'http://site.org/dir1/dir2/script', then this method will
1522   * return '<DocumentRoot>/dir1/dir2' where <DocumentRoot> is the filesystem path pointing
1523   * at this site's DocumentRoot.
1524   *
1525   * Note that the path is returned WITHOUT a trailing slash '/'.
1526   */
1527  public static function getRequestDir()
1528  {
1529    // see also: http://php.about.com/od/learnphp/qt/_SERVER_PHP.htm
1530    $path = str_replace('\\','/', $_SERVER['SCRIPT_NAME']);
1531    $root = FileManagerUtility::getSiteRoot();
1532    $path = dirname(!FileManagerUtility::startsWith($path, $root) ? $root . (!FileManagerUtility::startsWith($path, '/') ? '/' : '') . $path : $path);
1533    $path = (FileManagerUtility::endsWith($path,'/')) ? substr($path, 0, -1) : $path;
1534
1535    return $path;
1536  }
1537
1538  /**
1539   * Convert any relative or absolute path to a fully sanitized absolute path relative to DocumentRoot.
1540   *
1541   * When fed malicious paths (paths pointing outside the DocumentRoot tree) or paths which do not exist, a suitable
1542   * Exception will be thrown.
1543   * Note however that if and only if the parent directory of the given path does exist, is legal, and the
1544   * $mkdir_if_notexist argument is TRUE, then the last name in the path specification will be treated as a
1545   * subdirectory which will be created, while setting the directory permissions to the $chmod specified
1546   * value (default: 0777)
1547   */
1548  public static function getRealPath($path, $chmod = 0777, $mkdir_if_notexist = false, $with_trailing_slash = true)
1549  {
1550    $path = preg_replace('/(\\\|\/)+/', '/', $path);
1551    $root = FileManagerUtility::getSiteRoot();
1552    $path = str_replace($root,'',$path);
1553
1554    $path = (FileManagerUtility::startsWith($path,'/') ? $root . $path : FileManagerUtility::getRequestDir() . '/' . $path); /* do not base rel paths on FileManagerUtility::getRequestDir() root! */
1555
1556    /*
1557     * fold '../' directory parts to prevent malicious paths such as 'a/../../../../../../../../../etc/'
1558     * from succeeding
1559     *
1560     * to prevent screwups in the folding code, we FIRST clean out the './' directories, to prevent
1561     * 'a/./.././.././.././.././.././.././.././.././../etc/' from succeeding:
1562     */
1563    $path = preg_replace('#/(\./)+#','/',$path);
1564
1565    // now temporarily strip off the leading part up to the colon to prevent entries like '../d:/dir' to succeed when the site root is 'c:/', for example:
1566    $lead = '';
1567    // the leading part may NOT contain any directory separators, as it's for drive letters only.
1568    // So we must check in order to prevent malice like /../../../../../../../c:/dir from making it through.
1569    if (preg_match('#^([A-Za-z]:)?/(.*)$#', $path, $matches))
1570    {
1571        $lead = $matches[1];
1572        $path = '/' . $matches[2];
1573    }
1574
1575    while (($pos = strpos($path, '/..')) !== false)
1576    {
1577        $prev = substr($path, 0, $pos);
1578        /*
1579         * on Windows, you get:
1580         *
1581         * dirname("/") = "\"
1582         * dirname("y/") = "."
1583         * dirname("/x") = "\"
1584         *
1585         * so we'd rather not use dirname()   :-(
1586         */
1587        $p2 = strrpos($prev, '/');
1588        if ($p2 === false)
1589        {
1590            throw new FileManagerException('path_tampering');
1591        }
1592        $prev = substr($prev, 0, $p2);
1593        $next = substr($path, $pos + 3);
1594        if ($next && $next[0] != '/')
1595        {
1596            throw new FileManagerException('path_tampering');
1597        }
1598        $path = $prev . $next;
1599    }
1600
1601    $path = $lead . $path;
1602
1603    /*
1604     * iff there was such a '../../../etc/' attempt, we'll know because the resulting path does NOT have
1605     * the 'siteroot' prefix. We don't cover up such 'mishaps' but throw a tantrum instead, so upper level
1606     * logic can process this fact accordingly:
1607     */
1608    if (!FileManagerUtility::startsWith($path, $root))
1609    {
1610        throw new FileManagerException('path_tampering');
1611    }
1612
1613    if(!is_dir($path) && is_dir(dirname($path)) && $mkdir_if_notexist)
1614    {
1615        $rv = @mkdir($path,$chmod); // create last folder if not existing
1616        if ($rv === false)
1617        {
1618            throw new FileManagerException('mkdir_failed:' . $path);
1619        }
1620    }
1621
1622    /*
1623     * now all there's left for realpath() to do is expand possible symbolic links in the path; don't make
1624     * that dependent on how the path looks:
1625     */
1626    $rv = realpath($path);
1627    if ($rv === false)
1628    {
1629        throw new FileManagerException('realpath_failed:' . $path);
1630    }
1631    $path = str_replace('\\','/',$rv);
1632    $path = str_replace($root,'',$path);
1633    $path = ($with_trailing_slash ? (FileManagerUtility::endsWith($path,'/') ? $path : $path.'/') : ((FileManagerUtility::endsWith($path,'/') && strlen($path) > 1) ? substr($path, 0, -1) : $path));
1634
1635    return $path;
1636  }
1637
1638  /**
1639   * Return the filesystem absolute path equivalent of the output of getRealPath().
1640   */
1641  public static function getRealDir($path, $chmod = 0777, $mkdir_if_notexist = false, $with_trailing_slash = true)
1642  {
1643    $path = self::getRealPath($path, $chmod, $mkdir_if_notexist, $with_trailing_slash);
1644    $path = FileManagerUtility::getSiteRoot() . $path;
1645    return $path;
1646  }
1647
1648  /**
1649   * Apply rawurlencode() to each of the elements of the given path
1650   *
1651   * @note
1652   *   this method is provided as rawurlencode() tself also encodes the '/' separators in a path/string
1653   *   and we do NOT want to 'revert' such change with the risk of also picking up other %2F bits in
1654   *   the string (this assumes crafted paths can be fed to us).
1655   */
1656  public static function rawurlencode_path($path)
1657  {
1658    $encoded_path = explode('/', $path);
1659    array_walk($encoded_path, create_function('&$value', '$value = rawurlencode($value);'));
1660    return implode('/', $encoded_path);
1661  }
1662
1663  /**
1664   * Convert a number (representing number of bytes) to a formatted string representing GB .. bytes,
1665   * depending on the size of the value.
1666   */
1667  public static function fmt_bytecount($val, $precision = 1)
1668  {
1669    $unit = array('TB', 'GB', 'MB', 'KB', 'bytes');
1670    for ($x = count($unit) - 1; $val >= 1024 && $x > 0; $x--)
1671    {
1672        $val /= 1024.0;
1673    }
1674    $val = round($val, ($x > 0 ? $precision : 0));
1675    return $val . '&#160;' . $unit[$x];
1676  }
1677}
1678
Note: See TracBrowser for help on using the repository browser.