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

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

Further fixes for MFM.

File size: 143.1 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 *  - Ger Hobbelt (http://hebbut.net)
11 *  - James Sleeman (http://code.gogo.co.nz)
12 *
13 * License:
14 *   MIT-style license.
15 *
16 * Copyright:
17 *   Copyright (c) 2009-2011 [Christoph Pojer](http://cpojer.net)
18 *   Backend: FileManager & FMgr4Alias Copyright (c) 2011 [Ger Hobbelt](http://hobbelt.com)
19 *
20 * Dependencies:
21 *   - Tooling.php
22 *   - Image.class.php
23 *   - getId3 Library
24 *
25 * Options:
26 *   - directory: (string) The URI base directory to be used for the FileManager ('URI path' i.e. an absolute path here would be rooted at DocumentRoot: '/' == DocumentRoot)
27 *   - assetBasePath: (string, optional) The URI path to all images and swf files used by the filemanager
28 *   - thumbnailPath: (string) The URI path where the thumbnails of the pictures will be saved
29 *   - mimeTypesPath: (string, optional) The filesystem path to the MimeTypes.ini file. May exist in a place outside the DocumentRoot tree.
30 *   - dateFormat: (string, defaults to *j M Y - H:i*) The format in which dates should be displayed
31 *   - maxUploadSize: (integer, defaults to *20280000* bytes) The maximum file size for upload in bytes
32 *   - maxImageDimension: (array, defaults to *array('width' => 1024, 'height' => 768)*) The maximum number of pixels in height and width an image can have, if the user enables "resize on upload".
33 *   - upload: (boolean, defaults to *false*) allow uploads, this is also set in the FileManager.js (this here is only for security protection when uploads should be deactivated)
34 *   - destroy: (boolean, defaults to *false*) 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)
35 *   - create: (boolean, defaults to *false*) 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)
36 *   - move: (boolean, defaults to *false*) 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)
37 *   - download: (boolean, defaults to *false*) allow downloads, this is also set in the FileManager.js (this here is only for security protection when downloads should be deactivated)
38 *   - allowExtChange: (boolean, defaults to *false*) allow the file extension to be changed when performing a rename operation.
39 *   - safe: (boolean, defaults to *true*) If true, disallows 'exe', 'dll', 'php', 'php3', 'php4', 'php5', 'phps' and saves them as 'txt' instead.
40 *   - chmod: (integer, default is 0777) the permissions set to the uploaded files and created thumbnails (must have a leading "0", e.g. 0777)
41 *   - filter: (string, defaults to *null*) If not empty, this is a list of allowed mimetypes (overruled by the GET request 'filter' parameter: single requests can thus overrule the common setup in the constructor for this option)
42 *   - ViewIsAuthorized_cb (function/reference, default is *null*) authentication + authorization callback which can be used to determine whether the given directory may be viewed.
43 *     The parameter $action = 'view'.
44 *   - DetailIsAuthorized_cb (function/reference, default is *null*) authentication + authorization callback which can be used to determine whether the given file may be inspected (and the details listed).
45 *     The parameter $action = 'detail'.
46 *   - ThumbnailIsAuthorized_cb (function/reference, default is *null*) authentication + authorization callback which can be used to determine whether a thumbnail of the given file may be shown.
47 *     The parameter $action = 'thumbnail'.
48 *   - UploadIsAuthorized_cb (function/reference, default is *null*) authentication + authorization callback which can be used to determine whether the given file may be uploaded.
49 *     The parameter $action = 'upload'.
50 *   - DownloadIsAuthorized_cb (function/reference, default is *null*) authentication + authorization callback which can be used to determine whether the given file may be downloaded.
51 *     The parameter $action = 'download'.
52 *   - CreateIsAuthorized_cb (function/reference, default is *null*) authentication + authorization callback which can be used to determine whether the given subdirectory may be created.
53 *     The parameter $action = 'create'.
54 *   - 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.
55 *     The parameter $action = 'destroy'.
56 *   - 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.
57 *     Note that currently support for copying subdirectories is missing.
58 *     The parameter $action = 'move'.
59 *   - URIpropagateData (array, default is *null*) the data elements which will be passed along as part of the generated request URIs, i.e. the thumbnail request URIs. Use this to pass custom data elements to the
60 *     handler which delivers the thumbnails to the front-end.
61 *
62 * Obsoleted options:
63 *   - 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". (This option is obsoleted by the 'suggestedMaxImageDimension' option.)
64 *
65 *
66 * About the action permissions (upload|destroy|create|move|download):
67 *
68 *     All the option "permissions" are set to FALSE by default. Developers should always SPECIFICALLY enable a permission to have that permission, for two reasons:
69 *
70 *     1. Developers forget to disable permissions, they don't forget to enable them (because things don't work!)
71 *
72 *     2. Having open permissions by default leaves potential for security vulnerabilities where those open permissions are exploited.
73 *
74 *
75 * For all authorization hooks (callback functions) the following applies:
76 *
77 *     The callback should return TRUE for yes (permission granted), FALSE for no (permission denied).
78 *     Parameters sent to the callback are:
79 *       ($this, $action, $fileinfo)
80 *     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.
81 *     $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.
82 *
83 *     For more info about the hook parameter $fileinfo contents and a basic implementation, see further below (section 'Hooks: Detailed Interface Specification') and the examples in
84 *     Demos/FM-common.php, Demos/manager.php and Demos/selectImage.php
85 *
86 *
87 * Notes on relative paths and safety / security:
88 *
89 *   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,
90 *   i.e. dirname($_SERVER['SCRIPT_NAME']).
91 *
92 *   Requests may post/submit relative paths as arguments to their FileManager events/actions in $_GET/$_POST, and those relative paths will be
93 *   regarded as relative to the request URI handling script path, i.e. dirname($_SERVER['SCRIPT_NAME']) to make the most
94 *   sense from bother server and client coding perspective.
95 *
96 *
97 *   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
98 *   from succeeding. (An example of such would be an attacker posting his own 'destroy' event request requesting the destruction of
99 *   '../../../../../../../../../etc/passwd' for example. In more complex rigs, the attack may be assisted through attacks at these options' paths,
100 *   so these are subjected to the same scrutiny in here.)
101 *
102 *   All paths, absolute or relative, as passed to the event handlers (see the onXXX methods of this class) are ENFORCED TO ABIDE THE RULE
103 *   'every path resides within the options['directory'] a.k.a. BASEDIR rooted tree' without exception.
104 *   Because we can do without exceptions to important rules. ;-)
105 *
106 *   When paths apparently don't, they are coerced into adherence to this rule; when this fails, an exception is thrown internally and an error
107 *   will be reported and the action temrinated.
108 *
109 *  'LEGAL URL paths':
110 *
111 *   Paths which adhere to the aforementioned rule are so-called LEGAL URL paths; their 'root' equals BASEDIR.
112 *
113 *   BASEDIR equals the path pointed at by the options['directory'] setting. It is therefore imperative that you ensure this value is
114 *   correctly set up; worst case, this setting will equal DocumentRoot.
115 *   In other words: you'll never be able to reach any file or directory outside this site's DocumentRoot directory tree, ever.
116 *
117 *
118 *  Path transformations:
119 *
120 *   To allow arbitrary directory/path mapping algorithms to be applied (e.g. when implementing Alias support such as available in the
121 *   derived class FileManagerWithAliasSupport), all paths are, on every change/edit, transformed from their LEGAL URL representation to
122 *   their 'absolute URI path' (which is suitable to be used in links and references in HTML output) and 'absolute physical filesystem path'
123 *   equivalents.
124 *   By enforcing such a unidirectional transformation we implicitly support non-reversible and hard-to-reverse path aliasing mechanisms,
125 *   e.g. complex regex+context based path manipulations in the server.
126 *
127 *
128 *   When you need your paths to be restricted to the bounds of the options['directory'] tree (which is a subtree of the DocumentRoot based
129 *   tree), you may wish to use the 'legal' class of path transformation member functions:
130 *
131 *   - legal2abs_url_path()
132 *   - rel2abs_legal_url_path()
133 *   - legal_url_path2file_path()
134 *
135 *   When you have a 'absolute URI path' or a path relative in URI space (implicitly relative to dirname($_SERVER['SCRIPT_NAME']) ), you can
136 *   transform such a path to either a guaranteed-absolute URI space path or a filesystem path:
137 *
138 *   - rel2abs_url_path()
139 *   - url_path2file_path()
140 *
141 *   Any other path transformations are ILLEGAL and DANGEROUS. The only other possibly legal transformation is from absolute URI path to
142 *   BASEDIR-based LEGAL URL path, as the URI path space is assumed to be linear and contiguous. However, this operation is HIGHLY discouraged
143 *   as it is a very strong indicator of other faulty logic, so we do NOT offer a method for this.
144 *
145 *
146 * Hooks: Detailed Interface Specification:
147 *
148 *   All 'authorization' callback hooks share a common interface specification (function parameter set). This is by design, so one callback
149 *   function can be used to process any and all of these events:
150 *
151 *   Function prototype:
152 *
153 *       function CallbackFunction($mgr, $action, &$info)
154 *
155 *   where
156 *
157 *       $msg:      (object) reference to the current FileManager class instance. Can be used to invoke public FileManager methods inside
158 *                  the callback.
159 *
160 *       $action:   (string) identifies the event being processed. Can be one of these:
161 *
162 *                  'create'          create new directory
163 *                  'move'            move or copy a file or directory
164 *                  'destroy'         delete a file or directory
165 *                  'upload'          upload a single file (when performing a bulk upload, each file will be uploaded individually)
166 *                  'download'        download a file
167 *                  'view'            show a directory listing (in either 'list' or 'thumb' mode)
168 *                  'detail'          show detailed information about the file and, whn possible, provide a link to a (largish) thumbnail
169 *                  'thumbnail'       send the thumbnail to the client (done this way to allow JiT thumbnail creation)
170 *
171 *       $info      (array) carries all the details. Some of which can even be manipulated if your callbac is more than just an
172 *                  authentication / authorization checker. ;-)
173 *                  For more detail, see the next major section.
174 *
175 *   The callback should return a boolean, where TRUE means the session/client is authorized to execute the action, while FALSE
176 *   will cause the backend to report an authentication error and abort the action.
177 *
178 *  Exceptions throwing from the callback:
179 *
180 *   Note that you may choose to throw exceptions from inside the callback; those will be caught and transformed to proper error reports.
181 *
182 *   You may either throw any exceptions based on either the FileManagerException or Exception classes. When you format the exception
183 *   message as "XYZ:data", where 'XYZ' is a alphanumeric-only word, this will be transformed to a i18n-support string, where
184 *   'backend.XYZ' must map to a translation string (e.g. 'backend.nofile', see also the Language/Language.XX.js files) and the optional
185 *   'data' tail will be appended to the translated message.
186 *
187 *
188 * $info: the details:
189 *
190 *   Here is the list of $info members per $action event code:
191 *
192 *   'upload':
193 *
194 *           $info[] contains:
195 *
196 *               'legal_url'             (string) LEGAL URI path to the directory where the file is being uploaded. You may invoke
197 *                                           $dir = $mgr->legal_url_path2file_path($legal_url);
198 *                                       to obtain the physical filesystem path (also available in the 'dir' $info entry, by the way!), or
199 *                                           $url = $mgr->legal2abs_url_path($legal_url);
200 *                                       to obtain the absolute URI path for the given directory.
201 *
202 *               'dir'                   (string) physical filesystem path to the directory where the file is being uploaded.
203 *
204 *               'raw_filename'          (string) the raw, unprocessed filename of the file being being uploaded, as specified by the client.
205 *
206 *                                       WARNING: 'raw_filename' may contain anything illegal, such as directory paths instead of just a filename,
207 *                                                filesystem-illegal characters and what-not. Use 'name'+'extension' instead if you want to know
208 *                                                where the upload will end up.
209 *
210 *               'name'                  (string) the filename, sans extension, of the file being uploaded; this filename is ensured
211 *                                       to be both filesystem-legal, unique and not yet existing in the given directory.
212 *
213 *               'extension'             (string) the filename extension of the file being uploaded; this extension is ensured
214 *                                       to be filesystem-legal.
215 *
216 *                                       Note that the file name extension has already been cleaned, including 'safe' mode processing,
217 *                                       i.e. any uploaded binary executable will have been assigned the extension '.txt' already, when
218 *                                       FileManager's options['safe'] is enabled.
219 *
220 *               'tmp_filepath'          (string) filesystem path pointing at the temporary storage location of the uploaded file: you can
221 *                                       access the file data available here to optionally validate the uploaded content.
222 *
223 *               'mime'                  (string) the mime type as sniffed from the file
224 *
225 *               'mime_filter'           (optional, string) mime filter as specified by the client: a comma-separated string containing
226 *                                       full or partial mime types, where a 'partial' mime types is the part of a mime type before
227 *                                       and including the slash, e.g. 'image/'
228 *
229 *               'mime_filters'          (optional, array of strings) the set of allowed mime types, derived from the 'mime_filter' setting.
230 *
231 *               'size'                  (integer) number of bytes of the uploaded file
232 *
233 *               'maxsize'               (integer) the configured maximum number of bytes for any single upload
234 *
235 *               'overwrite'             (boolean) FALSE: the uploaded file will not overwrite any existing file, it will fail instead.
236 *
237 *                                       Set to TRUE (and adjust the 'name' and 'extension' entries as you desire) when you wish to overwrite
238 *                                       an existing file.
239 *
240 *               'chmod'                 (integer) UNIX access rights (default: 0666) for the file-to-be-created (RW for user,group,world).
241 *
242 *                                       Note that the eXecutable bits have already been stripped before the callback was invoked.
243 *
244 *
245 *         Note that this request originates from a Macromedia Flash client: hence you'll need to use the
246 *         $_POST[session_name()] value to manually set the PHP session_id() before you start your your session
247 *         again.
248 *
249 *         The frontend-specified options.propagateData items will be available as $_GET[] items.
250 *
251 *         The frontend-specified options.uploadAuthData items will be available as $_POST[] items.
252 *
253 *
254 *  'download':
255 *
256 *           $info[] contains:
257 *
258 *               'legal_url'             (string) LEGAL URI path to the file to be downloaded. You may invoke
259 *                                           $dir = $mgr->legal_url_path2file_path($legal_url);
260 *                                       to obtain the physical filesystem path (also available in the 'file' $info entry, by the way!), or
261 *                                           $url = $mgr->legal2abs_url_path($legal_url);
262 *                                       to obtain the absolute URI path for the given file.
263 *
264 *               'file'                  (string) physical filesystem path to the file being downloaded.
265 *
266 *               'mime'                  (string) the mime type as sniffed from the file
267 *
268 *               'mime_filter'           (optional, string) mime filter as specified by the client: a comma-separated string containing
269 *                                       full or partial mime types, where a 'partial' mime types is the part of a mime type before
270 *                                       and including the slash, e.g. 'image/'
271 *
272 *               'mime_filters'          (optional, array of strings) the set of allowed mime types, derived from the 'mime_filter' setting.
273 *
274 *         The frontend-specified options.propagateData items will be available as $_GET[] items.
275 *
276 *
277 *  'create': // create directory
278 *
279 *           $info[] contains:
280 *
281 *               'legal_url'             (string) LEGAL URI path to the parent directory of the directory being created. You may invoke
282 *                                           $dir = $mgr->legal_url_path2file_path($legal_url);
283 *                                       to obtain the physical filesystem path (also available in the 'dir' $info entry, by the way!), or
284 *                                           $url = $mgr->legal2abs_url_path($legal_url);
285 *                                       to obtain the absolute URI path for this parent directory.
286 *
287 *               'dir'                   (string) physical filesystem path to the parent directory of the directory being created.
288 *
289 *               'raw_name'              (string) the name of the directory to be created, as specified by the client (unfiltered!)
290 *
291 *               'uniq_name'             (string) the name of the directory to be created, filtered and ensured to be both unique and
292 *                                       not-yet-existing in the filesystem.
293 *
294 *               'newdir'                (string) the filesystem absolute path to the directory to be created; identical to:
295 *                                           $newdir = $mgr->legal_url_path2file_path($legal_url . $uniq_name);
296 *                                       Note the above: all paths are transformed from URI space to physical disk every time a change occurs;
297 *                                       this allows us to map even not-existing 'directories' to possibly disparate filesystem locations.
298 *
299 *               'chmod'                 (integer) UNIX access rights (default: 0777) for the directory-to-be-created (RWX for user,group,world)
300 *
301 *         The frontend-specified options.propagateData items will be available as $_GET[] items.
302 *
303 *
304 *  'destroy':
305 *
306 *           $info[] contains:
307 *
308 *               'legal_url'             (string) LEGAL URI path to the file/directory to be deleted. You may invoke
309 *                                           $dir = $mgr->legal_url_path2file_path($legal_url);
310 *                                       to obtain the physical filesystem path (also available in the 'file' $info entry, by the way!), or
311 *                                           $url = $mgr->legal2abs_url_path($legal_url);
312 *                                       to obtain the absolute URI path for the given file/directory.
313 *
314 *               'file'                  (string) physical filesystem path to the file/directory being deleted.
315 *
316 *               'mime'                  (string) the mime type as sniffed from the file / directory (directories are mime type: 'text/directory')
317 *
318 *               'mime_filter'           (optional, string) mime filter as specified by the client: a comma-separated string containing
319 *                                       full or partial mime types, where a 'partial' mime types is the part of a mime type before
320 *                                       and including the slash, e.g. 'image/'
321 *
322 *               'mime_filters'          (optional, array of strings) the set of allowed mime types, derived from the 'mime_filter' setting.
323 *
324 *                                       Note that the 'mime_filters', if any, are applied to the 'delete' operation in a special way: only
325 *                                       files matching one of the mime types in this list will be deleted; anything else will remain intact.
326 *                                       This can be used to selectively clean a directory tree.
327 *
328 *                                       The design idea behind this approach is that you are only allowed what you can see ('view'), so
329 *                                       all 'view' restrictions should equally to the 'delete' operation.
330 *
331 *         The frontend-specified options.propagateData items will be available as $_GET[] items.
332 *
333 *
334 *  'move':  // move or copy!
335 *
336 *           $info[] contains:
337 *
338 *               'legal_url'             (string) LEGAL URI path to the source parent directory of the file/directory being moved/copied. You may invoke
339 *                                           $dir = $mgr->legal_url_path2file_path($legal_url);
340 *                                       to obtain the physical filesystem path (also available in the 'dir' $info entry, by the way!), or
341 *                                           $url = $mgr->legal2abs_url_path($legal_url);
342 *                                       to obtain the absolute URI path for the given directory.
343 *
344 *               'dir'                   (string) physical filesystem path to the source parent directory of the file/directory being moved/copied.
345 *
346 *               'path'                  (string) physical filesystem path to the file/directory being moved/copied itself; this is the full source path.
347 *
348 *               'name'                  (string) the name itself of the file/directory being moved/copied; this is the source name.
349 *
350 *               'legal_newurl'          (string) LEGAL URI path to the target parent directory of the file/directory being moved/copied. You may invoke
351 *                                           $dir = $mgr->legal_url_path2file_path($legal_url);
352 *                                       to obtain the physical filesystem path (also available in the 'dir' $info entry, by the way!), or
353 *                                           $url = $mgr->legal2abs_url_path($legal_url);
354 *                                       to obtain the absolute URI path for the given directory.
355 *
356 *               'newdir'                (string) physical filesystem path to the target parent directory of the file/directory being moved/copied;
357 *                                       this is the full path of the directory where the file/directory will be moved/copied to. (filesystem absolute)
358 *
359 *               'newpath'               (string) physical filesystem path to the target file/directory being moved/copied itself; this is the full destination path,
360 *                                       i.e. the full path of where the file/directory should be renamed/moved to. (filesystem absolute)
361 *
362 *               'newname'               (string) the target name itself of the file/directory being moved/copied; this is the destination name.
363 *
364 *                                       This filename is ensured to be both filesystem-legal, unique and not yet existing in the given target directory.
365 *
366 *               'rename'                (boolean) TRUE when a file/directory RENAME operation is requested (name change, staying within the same
367 *                                       parent directory). FALSE otherwise.
368 *
369 *               'is_dir'                (boolean) TRUE when the subject is a directory itself, FALSE when it is a regular file.
370 *
371 *               'function'              (string) PHP call which will perform the operation. ('rename' or 'copy')
372 *
373 *         The frontend-specified options.propagateData items will be available as $_GET[] items.
374 *
375 *
376 *  'view':
377 *
378 *           $info[] contains:
379 *
380 *               'legal_url'             (string) LEGAL URI path to the directory being viewed/scanned. You may invoke
381 *                                           $dir = $mgr->legal_url_path2file_path($legal_url);
382 *                                       to obtain the physical filesystem path (also available in the 'dir' $info entry, by the way!), or
383 *                                           $url = $mgr->legal2abs_url_path($legal_url);
384 *                                       to obtain the absolute URI path for the scanned directory.
385 *
386 *               'dir'                   (string) physical filesystem path to the directory being viewed/scanned.
387 *
388 *               'files'                 (array of strings) array of files and directories (including '..' entry at the top when this is a
389 *                                       subdirectory of the FM-managed tree): only names, not full paths.
390 *
391 *               'mime_filter'           (optional, string) mime filter as specified by the client: a comma-separated string containing
392 *                                       full or partial mime types, where a 'partial' mime types is the part of a mime type before
393 *                                       and including the slash, e.g. 'image/'
394 *
395 *               'mime_filters'          (optional, array of strings) the set of allowed mime types, derived from the 'mime_filter' setting.
396 *
397 *               'guess_mime'            (boolean) TRUE when the mime type for each file in this directory will be determined using filename
398 *                                       extension sniffing only; FALSE means the mime type will be determined using content sniffing, which
399 *                                       is slower.
400 *
401 *               'list_type'             (string) the type of view requested: 'list' or 'thumb'.
402 *
403 *               'file_preselect'        (optional, string) filename of a file in this directory which should be located and selected.
404 *                                       When found, the backend will provide an index number pointing at the corresponding JSON files[]
405 *                                       entry to assist the front-end in jumping to that particular item in the view.
406 *
407 *               'preliminary_json'      (array) the JSON data collected so far; when ['status']==1, then we're performing a regular view
408 *                                       operation (possibly as the second half of a copy/move/delete operation), when the ['status']==0,
409 *                                       we are performing a view operation as the second part of another otherwise failed action, e.g. a
410 *                                       failed 'create directory'.
411 *
412 *         The frontend-specified options.propagateData items will be available as $_GET[] items.
413 *
414 *
415 *  'detail':
416 *
417 *           $info[] contains:
418 *
419 *               'legal_url'             (string) LEGAL URI path to the file/directory being inspected. You may invoke
420 *                                           $dir = $mgr->legal_url_path2file_path($legal_url);
421 *                                       to obtain the physical filesystem path (also available in the 'file' $info entry, by the way!), or
422 *                                           $url = $mgr->legal2abs_url_path($legal_url);
423 *                                       to obtain the absolute URI path for the given file.
424 *
425 *               'file'                  (string) physical filesystem path to the file being inspected.
426 *
427 *               'filename'              (string) the filename of the file being inspected. (Identical to 'basename($legal_url)')
428 *
429 *               'mime'                  (string) the mime type as sniffed from the file
430 *
431 *               'mime_filter'           (optional, string) mime filter as specified by the client: a comma-separated string containing
432 *                                       full or partial mime types, where a 'partial' mime types is the part of a mime type before
433 *                                       and including the slash, e.g. 'image/'
434 *
435 *               'mime_filters'          (optional, array of strings) the set of allowed mime types, derived from the 'mime_filter' setting.
436 *
437 *         The frontend-specified options.propagateData items will be available as $_GET[] items.
438 *
439 *
440 *  'thumbnail':
441 *
442 *           $info[] contains:
443 *
444 *               'legal_url'             (string) LEGAL URI path to the file/directory being thumbnailed. You may invoke
445 *                                           $dir = $mgr->legal_url_path2file_path($legal_url);
446 *                                       to obtain the physical filesystem path (also available in the 'file' $info entry, by the way!), or
447 *                                           $url = $mgr->legal2abs_url_path($legal_url);
448 *                                       to obtain the absolute URI path for the given file.
449 *
450 *               'file'                  (string) physical filesystem path to the file being inspected.
451 *
452 *               'filename'              (string) the filename of the file being inspected. (Identical to 'basename($legal_url)')
453 *
454 *               'mime'                  (string) the mime type as sniffed from the file
455 *
456 *               'mime_filter'           (optional, string) mime filter as specified by the client: a comma-separated string containing
457 *                                       full or partial mime types, where a 'partial' mime types is the part of a mime type before
458 *                                       and including the slash, e.g. 'image/'
459 *
460 *               'mime_filters'          (optional, array of strings) the set of allowed mime types, derived from the 'mime_filter' setting.
461 *The parameter $action = 'thumbnail'.
462 *   - UploadIsAuthorized_cb (function/reference, default is *null*) authentication + authorization callback which can be used to determine whether the given file may be uploaded.
463 *               'requested_size'        (integer) the size (maximum width and height) in pixels of the thumbnail to be produced.
464 *
465 *         The frontend-specified options.propagateData items will be available as $_GET[] items.
466 *
467 *
468 *
469 * Developer Notes:
470 *
471 * - member functions which have a commented out 'static' keyword have it removed by design: it makes for easier overloading through
472 *   inheritance that way and meanwhile there's no pressing need to have those (public) member functions acccessible from the outside world
473 *   without having an instance of the FileManager class itself round at the same time.
474 */
475
476// ----------- compatibility checks ----------------------------------------------------------------------------
477if (version_compare(PHP_VERSION, '5.2.0') < 0)
478{
479        // die horribly: server does not match our requirements!
480        header('HTTP/1.0 500 FileManager requires PHP 5.2.0 or later', true, 500); // Internal server error
481        throw Exception('FileManager requires PHP 5.2.0 or later');   // this exception will most probably not be caught; that's our intent!
482}
483
484if (function_exists('UploadIsAuthenticated'))
485{
486        // die horribly: user has not upgraded his callback hook(s)!
487        header('HTTP/1.0 500 FileManager callback has not been upgraded!', true, 500); // Internal server error
488        throw Exception('FileManager callback has not been upgraded!');   // this exception will most probably not be caught; that's our intent!
489}
490
491//-------------------------------------------------------------------------------------------------------------
492
493if (!defined('DEVELOPMENT')) define('DEVELOPMENT', 0);   // make sure this #define is always known to us
494
495
496
497require_once(str_replace('\\', '/', dirname(__FILE__)) . '/Tooling.php');
498require_once(str_replace('\\', '/', dirname(__FILE__)) . '/Image.class.php');
499require_once(str_replace('\\', '/', dirname(__FILE__)) . '/Assets/getid3/getid3.php');
500
501
502
503// the jpeg quality for the largest thumbnails (smaller ones are automatically done at increasingly higher quality)
504define('MTFM_THUMBNAIL_JPEG_QUALITY', 75);
505
506// the number of directory levels in the thumbnail cache; set to 2 when you expect to handle huge image collections.
507//
508// Note that each directory level distributes the files evenly across 256 directories; hence, you may set this
509// level count to 2 when you expect to handle more than 32K images in total -- as each image will have two thumbnails:
510// a 48px small one and a 250px large one.
511define('MTFM_NUMBER_OF_DIRLEVELS_FOR_CACHE', 1);
512
513
514class FileManager
515{
516        protected $options;
517
518        public function __construct($options)
519        {
520                $this->options = array_merge(array(
521                        /*
522                         * Note that all default paths as listed below are transformed to DocumentRoot-based paths
523                         * through the getRealPath() invocations further below:
524                         */
525                        'directory' => null,                                       // MUST be in the DocumentRoot tree
526                        'assetBasePath' => null,                                   // may sit outside options['directory'] but MUST be in the DocumentRoot tree
527                        'thumbnailPath' => null,                                   // may sit outside options['directory'] but MUST be in the DocumentRoot tree
528                        'mimeTypesPath' => str_replace('\\', '/', dirname(__FILE__)) . '/MimeTypes.ini',   // an absolute filesystem path anywhere; when relative, it will be assumed to be against SERVER['SCRIPT_NAME']
529                        'dateFormat' => 'j M Y - H:i',
530                        'maxUploadSize' => 2600 * 2600 * 3,
531                        // 'maxImageSize' => 99999,                                 // obsoleted, replaced by 'suggestedMaxImageDimension'
532                        // Xinha: Allow to specify the "Resize Large Images" tolerance level.
533                        'maxImageDimension' => array('width' => 1024, 'height' => 768),
534                        'upload' => false,
535                        'destroy' => false,
536                        'create' => false,
537                        'move' => false,
538                        'download' => false,
539                        /* ^^^ this last one is easily circumnavigated if it's about images: when you can view 'em, you can 'download' them anyway.
540                         *     However, for other mime types which are not previewable / viewable 'in their full bluntal nugity' ;-) , this will
541                         *     be a strong deterent.
542                         *
543                         *     Think Springer Verlag and PDFs, for instance. You can have 'em, but only /after/ you've ...
544                         */
545                        'allowExtChange' => false,
546                        'safe' => true,
547                        'filter' => null,
548                        'chmod' => 0777,
549                        'ViewIsAuthorized_cb' => null,
550                        'DetailIsAuthorized_cb' => null,
551                        'ThumbnailIsAuthorized_cb' => null,
552                        'UploadIsAuthorized_cb' => null,
553                        'DownloadIsAuthorized_cb' => null,
554                        'CreateIsAuthorized_cb' => null,
555                        'DestroyIsAuthorized_cb' => null,
556                        'MoveIsAuthorized_cb' => null,
557                        'thumbnailsMustGoThroughBackend' => true, // If set true (default) all thumbnail requests go through the backend (onThumbnail), if false, thumbnails will "shortcircuit" if they exist, saving roundtrips when using POST type propagateData
558                        'showHiddenFoldersAndFiles'      => false, // Hide dot dirs/files ?
559                        'URIpropagateData' => null
560                ), (is_array($options) ? $options : array()));
561
562                // transform the obsoleted/deprecated options:
563                if (!empty($this->options['maxImageSize']) && $this->options['maxImageSize'] != 1024 && $this->options['maxImageDimension']['width'] == 1024 && $this->options['maxImageDimension']['height'] == 768)
564                {
565                        $this->options['maxImageDimension'] = array('width' => $this->options['maxImageSize'], 'height' => $this->options['maxImageSize']);
566                }
567
568                // only calculate the guestimated defaults when they are indeed required:
569                if ($this->options['directory'] == null || $this->options['assetBasePath'] == null || $this->options['thumbnailPath'] == null)
570                {
571                        $assumed_root = @realpath($_SERVER['DOCUMENT_ROOT']);
572                        $assumed_root = str_replace('\\', '/', $assumed_root);
573                        if (FileManagerUtility::endsWith($assumed_root, '/'))
574                        {
575                                $assumed_root = substr($assumed_root, 0, -1);
576                        }
577                        $my_path = @realpath(dirname(__FILE__));
578                        $my_path = str_replace('\\', '/', $my_path);
579                        if (!FileManagerUtility::endsWith($my_path, '/'))
580                        {
581                                $my_path .= '/';
582                        }
583                        $my_assumed_url_path = str_replace($assumed_root, '', $my_path);
584
585                        // we throw an Exception here because when these do not apply, the user should have specified all three these entries!
586                        if (empty($assumed_root) || empty($my_path) || !FileManagerUtility::startsWith($my_path, $assumed_root))
587                                throw new FileManagerException('nofile');
588
589                        if ($this->options['directory'] == null)
590                        {
591                                $this->options['directory'] = $my_assumed_url_path . '../../Demos/Files/';
592                        }
593                        if ($this->options['assetBasePath'] == null)
594                        {
595                                $this->options['assetBasePath'] = $my_assumed_url_path . '../../Demos/Files/../../Assets/';
596                        }
597                        if ($this->options['thumbnailPath'] == null)
598                        {
599                                $this->options['thumbnailPath'] = $my_assumed_url_path . '../../Demos/Files/../../Assets/Thumbs/';
600                        }
601                }
602
603                /*
604                 * make sure we start with a very predictable and LEGAL options['directory'] setting, so that the checks applied to the
605                 * (possibly) user specified value for this bugger acvtually can check out okay AS LONG AS IT'S INSIDE the DocumentRoot-based
606                 * directory tree:
607                 */
608                $new_root = $this->options['directory'];
609                $this->options['directory'] = '/';      // use DocumentRoot temporarily as THE root for this optional transform
610                $this->options['directory'] = self::enforceTrailingSlash($this->rel2abs_url_path($new_root));
611
612                // now that the correct options['directory'] has been set up, go and check/clean the other paths in the options[]:
613
614                $this->options['thumbnailPath'] = self::enforceTrailingSlash($this->rel2abs_url_path($this->options['thumbnailPath']));
615                $this->options['assetBasePath'] = self::enforceTrailingSlash($this->rel2abs_url_path($this->options['assetBasePath']));
616
617                $this->options['mimeTypesPath'] = @realpath($this->options['mimeTypesPath']);
618                if (empty($this->options['mimeTypesPath']))
619                        throw new FileManagerException('nofile');
620                $this->options['mimeTypesPath'] = str_replace('\\', '/', $this->options['mimeTypesPath']);
621
622                if (!headers_sent())
623                {
624                        header('Expires: Fri, 01 Jan 1990 00:00:00 GMT');
625                        header('Cache-Control: no-cache, no-store, max-age=0, must-revalidate');
626                }
627        }
628
629        /**
630         * @return array the FileManager options and settings.
631         */
632        public function getSettings()
633        {
634                return array_merge(array(
635                                'basedir' => $this->url_path2file_path($this->options['directory'])
636                ), $this->options);
637        }
638
639
640
641
642        /**
643         * Central entry point for any client side request.
644         */
645        public function fireEvent($event = null)
646        {
647                $event = !empty($event) ? 'on' . ucfirst($event) : null;
648                if (!$event || !method_exists($this, $event)) $event = 'onView';
649
650                $this->{$event}();
651        }
652
653
654
655
656
657
658        /**
659         * Generalized 'view' handler, which produces a directory listing.
660         *
661         * Return the directory listing in a nested array, suitable for JSON encoding.
662         */
663        protected function _onView($legal_url, $json, $mime_filter, $list_type, $file_preselect_arg = null, $filemask = '*')
664        {
665                $dir = $this->legal_url_path2file_path($legal_url);
666                if (!is_dir($dir))
667                {
668                        throw new FileManagerException('nofile');
669                }
670                $files = $this->scandir($dir, $filemask);
671
672    // This can not be done in scandir, because other stuff (particularly delete)
673    // might need those dotfiles.
674    if(!$this->options['showHiddenFoldersAndFiles'])
675    {
676      $nohidden = array();
677      foreach($files as $file)
678      {
679        if($file[0] == '.' && $file != '.' & $file != '..') continue;
680        $nohidden[] = $file;
681      }
682      $files = $nohidden;
683    }
684
685                if ($files === false)
686                        throw new FileManagerException('nofile');
687
688                /*
689                 * To ensure '..' ends up at the very top of the view, no matter what the other entries in $files[] are made of,
690                 * we pop the last element off the array, check whether it's the double-dot, and if so, keep it out while we
691                 * let the sort run.
692                 */
693                $doubledot = array_pop($files);
694                if ($doubledot !== null && $doubledot !== '..')
695                {
696                        $files[] = $doubledot;
697                        $doubledot = null;
698                }
699                natcasesort($files);
700                if ($doubledot !== null)
701                {
702                        array_unshift($files, $doubledot);
703                }
704
705                $mime_filters = $this->getAllowedMimeTypes($mime_filter);
706
707                // remove the imageinfo() call overhead per file for very large directories; just guess at the mimetye from the filename alone.
708                // The real mimetype will show up in the 'details' view anyway! This is only for the 'filter' function:
709                $just_guess_mime = (count($files) > 100);
710
711                $fileinfo = array(
712                                'legal_url' => $legal_url,
713                                'dir' => $dir,
714                                'files' => $files,
715                                'mime_filter' => $mime_filter,
716                                'mime_filters' => $mime_filters,
717                                'guess_mime' => $just_guess_mime,
718                                'list_type' => $list_type,
719                                'file_preselect' => $file_preselect_arg,
720                                'preliminary_json' => $json
721                        );
722
723                if (!empty($this->options['ViewIsAuthorized_cb']) && function_exists($this->options['ViewIsAuthorized_cb']) && !$this->options['ViewIsAuthorized_cb']($this, 'view', $fileinfo))
724                        throw new FileManagerException('authorized');
725
726                $legal_url = $fileinfo['legal_url'];
727                $dir = $fileinfo['dir'];
728                $files = $fileinfo['files'];
729                $mime_filter = $fileinfo['mime_filter'];
730                $mime_filters = $fileinfo['mime_filters'];
731                $just_guess_mime = $fileinfo['guess_mime'];
732                $list_type = $fileinfo['list_type'];
733                $file_preselect_arg = $fileinfo['file_preselect'];
734                $json = $fileinfo['preliminary_json'];
735
736                $file_preselect_index = -1;
737                $idx = array(0, 0);
738
739                foreach ($files as $filename)
740                {
741                        $url = $legal_url . $filename;
742                        // must transform here so alias/etc. expansions inside legal_url_path2file_path() get a chance:
743                        $file = $this->legal_url_path2file_path($url);
744
745                        $isdir = (is_file($file) ? 0 : 1);
746                        if (!$isdir)
747                        {
748                                $mime = $this->getMimeType($file, $just_guess_mime);
749                                if (is_file($file))
750                                {
751                                        if (!$this->IsAllowedMimeType($mime, $mime_filters))
752                                                continue;
753                                }
754                                else
755                                {
756                                        continue;
757                                }
758                                $iconspec = $filename;
759
760                                if ($filename == $file_preselect_arg)
761                                {
762                                        $file_preselect_index = $idx[0];
763                                }
764                        }
765                        else if (is_dir($file))
766                        {
767                                $mime = 'text/directory';
768                                $iconspec = ($filename == '..' ? 'is.dir_up' : 'is.dir');
769                        }
770                        else
771                        {
772                                // simply do NOT list anything that we cannot cope with.
773                                // That includes clearly inaccessible files (and paths) with non-ASCII characters:
774                                // PHP5 and below are a real mess when it comes to handling Unicode filesystems
775                                // (see the php.net site too: readdir / glob / etc. user comments and the official
776                                // notice that PHP will support filesystem UTF-8/Unicode only when PHP6 is released.
777                                //
778                                // Big, fat bummer!
779                                continue;
780                        }
781
782                        if (FileManagerUtility::startsWith($mime, 'image/'))
783                        {
784                                /*
785                                 * offload the thumbnailing process to another event ('event=thumbnail') to be fired by the client
786                                 * when it's time to render the thumbnail: the offloading helps us tremendously in coping with large
787                                 * directories:
788                                 * WE simply assume the thumbnail will be there, so we don't even need to check for its existence
789                                 * (which saves us one more file_exists() per item at the very least). And when it doesn't, that's
790                                 * for the event=thumbnail handler to worry about (creating the thumbnail on demand or serving
791                                 * a generic icon image instead).
792                                 */
793                               
794                                unset($thumb48, $thumb250);
795                                if(!$this->options['thumbnailsMustGoThroughBackend'])
796        {
797          try
798          { 
799            $thumb48  = $this->getThumb ($url, $file, 48, 48, true);
800            $thumb250 = $this->getThumb ($url, $file, 250, 250, true);
801          }
802          catch(Exception $E)
803          {
804            // Fallback to event request
805          }
806        }
807                                if(!isset($thumb48))
808                                $thumb48 = $this->mkEventHandlerURL(array(
809                                                'event' => 'thumbnail',
810                                                // directory and filename of the ORIGINAL image should follow next:
811                                                'directory' => $legal_url,
812                                                'file' => $filename,
813                                                'size' => 48,          // thumbnail suitable for 'view/type=thumb' list views
814                                                'filter' => $mime_filter
815                                        ));
816                                       
817        if(!isset($thumb250))
818                                $thumb250 = $this->mkEventHandlerURL(array(
819                                                'event' => 'thumbnail',
820                                                // directory and filename of the ORIGINAL image should follow next:
821                                                'directory' => $legal_url,
822                                                'file' => $filename,
823                                                'size' => 250,         // thumbnail suitable for 'view/type=thumb' list views
824                                                'filter' => $mime_filter
825                                        ));
826                        }
827                        else
828                        {
829                                $thumb48 = FileManagerUtility::rawurlencode_path($this->getIcon($iconspec, false));
830                                $thumb250 = $thumb48;
831                        }
832                        $icon = FileManagerUtility::rawurlencode_path($this->getIcon($iconspec, true));
833
834                        if ($list_type == 'thumb')
835                        {
836                                $thumb = $thumb48;
837                        }
838                        else
839                        {
840                                $thumb = $icon;
841                        }
842
843                        $out[$isdir][] = array(
844                                        'path' => FileManagerUtility::rawurlencode_path($url),
845                                        'name' => preg_replace('/[^ -~]/', '?', $filename),       // HACK/TWEAK: PHP5 and below are completely b0rked when it comes to international filenames   :-(
846                                        'date' => date($this->options['dateFormat'], @filemtime($file)),
847                                        'mime' => $mime,
848                                        'thumbnail' => $thumb,
849                                        'thumbnail48' => $thumb48,
850                                        'thumbnail250' => $thumb250,
851                                        'icon' => $icon,
852                                        'size' => @filesize($file)
853                                );
854                        $idx[$isdir]++;
855
856                        if (0)
857                        {
858                                // help PHP when 'doing' large image directories: reset the timeout for each thumbnail / entry we produce:
859                                //   http://www.php.net/manual/en/info.configuration.php#ini.max-execution-time
860                                set_time_limit(max(30, ini_get('max_execution_time')));
861                        }
862                }
863
864                $thumb48 = FileManagerUtility::rawurlencode_path($this->getIcon('is.dir', false));
865                $icon = FileManagerUtility::rawurlencode_path($this->getIcon('is.dir', true));
866                if ($list_type == 'thumb')
867                {
868                        $thumb = $thumb48;
869                }
870                else
871                {
872                        $thumb = $icon;
873                }
874                return array(
875                        'dirs' => (!empty($out[1]) ? $out[1] : array()),
876                        'files' => (!empty($out[0]) ? $out[0] : array()),
877                        'json' => array_merge((is_array($json) ? $json : array()), array(
878                                'root' => substr($this->options['directory'], 1),
879                                'path' => $legal_url,                                  // is relative to options['directory']
880                                'dir' => array(
881                                        'path' => FileManagerUtility::rawurlencode_path($legal_url),
882                                        'name' => pathinfo($legal_url, PATHINFO_BASENAME),
883                                        'date' => date($this->options['dateFormat'], @filemtime($dir)),
884                                        'mime' => 'text/directory',
885                                        'thumbnail' => $thumb,
886                                        'thumbnail48' => $thumb48,
887                                        'thumbnail250' => $thumb48,
888                                        'icon' => $icon
889                                ),
890                                'preselect_index' => $file_preselect_index,
891                                'preselect_name' => ($file_preselect_index >= 0 ? $file_preselect_arg : null)
892                        ))
893                );
894        }
895
896        /**
897         * Process the 'view' event (default event fired by fireEvent() method)
898         *
899         * Returns a JSON encoded directory view list.
900         *
901         * Expected parameters:
902         *
903         * $_POST['directory']     path relative to basedir a.k.a. options['directory'] root
904         *
905         * $_POST['file_preselect']     optional filename or path:
906         *                         when a filename, this is the filename of a file in this directory
907         *                         which should be located and selected. When found, the backend will
908         *                         provide an index number pointing at the corresponding JSON files[]
909         *                         entry to assist the front-end in jumping to that particular item
910         *                         in the view.
911         *
912         *                         when a path, it is either an absolute or a relative path:
913         *                         either is assumed to be a URI URI path, i.e. rooted at
914         *                           DocumentRoot.
915         *                         The path will be transformed to a LEGAL URI path and
916         *                         will OVERRIDE the $_POST['directory'] path.
917         *                         Otherwise, this mode acts as when only a filename was specified here.
918         *                         This mode is useful to help a frontend to quickly jump to a file
919         *                         pointed at by a URI.
920         *
921         *                         N.B.: This also the only entry which accepts absolute URI paths and
922         *                               transforms them to LEGAL URI paths.
923         *
924         *                         When the specified path is illegal, i.e. does not reside inside the
925         *                         options['directory']-rooted LEGAL URI subtree, it will be discarded
926         *                         entirely (as all file paths, whether they are absolute or relative,
927         *                         must end up inside the options['directory']-rooted subtree to be
928         *                         considered manageable files) and the process will continue as if
929         *                         the $_POST['file_preselect'] entry had not been set.
930         *
931         * $_POST['filter']        optional mimetype filter string, amy be the part up to and
932         *                         including the slash '/' or the full mimetype. Only files
933         *                         matching this (set of) mimetypes will be listed.
934         *                         Examples: 'image/' or 'application/zip'
935         *
936         * $_POST['type']          'thumb' will produce a list view including thumbnail and other
937         *                         information with each listed file; other values will produce
938         *                         a basic list view (similar to Windows Explorer 'list' view).
939         *
940         * Errors will produce a JSON encoded error report, including at least two fields:
941         *
942         * status                  0 for error; nonzero for success
943         *
944         * error                   error message
945         *
946         * Next to these, the JSON encoded output will, with high probability, include a
947         * list view of the parent or 'basedir' as a fast and easy fallback mechanism for client side
948         * viewing code. However, severe and repetitive errors may not produce this
949         * 'fallback view list' so proper client code should check the 'status' field in the
950         * JSON output.
951         */
952        protected function onView()
953        {
954                // try to produce the view; if it b0rks, retry with the parent, until we've arrived at the basedir:
955                // then we fail more severely.
956
957                $emsg = null;
958                $jserr = array(
959                                'status' => 1
960                        );
961
962                $mime_filter = $this->getPOSTparam('filter', $this->options['filter']);
963                $list_type = ($this->getPOSTparam('type') != 'thumb' ? 'list' : 'thumb');
964
965                try
966                {
967                        $dir_arg = $this->getPOSTparam('directory');
968                        $legal_url = $this->rel2abs_legal_url_path($dir_arg);
969                        $legal_url = self::enforceTrailingSlash($legal_url);
970
971                        $file_preselect_arg = $this->getPOSTparam('file_preselect');
972                        try
973                        {
974                                if (!empty($file_preselect_arg))
975                                {
976                                        // check if this a path instead of just a basename, then convert to legal_url and split across filename and directory.
977                                        if (strpos($file_preselect_arg, '/') !== false)
978                                        {
979                                                // this will also convert a relative path to an absolute path before transforming it to a LEGAL URI path:
980                                                $legal_presel = $this->abs2legal_url_path($file_preselect_arg);
981                                               
982                                                $prseli = pathinfo($legal_presel);
983                                                $file_preselect_arg = $prseli['basename'];
984                                                // override the directory!
985                                                $legal_url = $prseli['dirname'];
986                                                $legal_url = self::enforceTrailingSlash($legal_url);
987                                        }
988                                        else
989                                        {
990                                                $file_preselect_arg = pathinfo($file_preselect_arg, PATHINFO_BASENAME);
991                                        }
992                                }
993                        }
994                        catch(FileManagerException $e)
995                        {
996                                // discard the preselect input entirely:
997                                $file_preselect_arg = null;
998                        }
999                }
1000                catch(FileManagerException $e)
1001                {
1002                        $emsg = $e->getMessage();
1003                        $legal_url = '/';
1004                        $file_preselect_arg = null;
1005                }
1006                catch(Exception $e)
1007                {
1008                        // catching other severe failures; since this can be anything it may not be a translation keyword in the message...
1009                        $emsg = $e->getMessage();
1010                        $legal_url = '/';
1011                        $file_preselect_arg = null;
1012                }
1013
1014                // loop until we drop below the bottomdir; meanwhile getDir() above guarantees that $dir is a subdir of bottomdir, hence dir >= bottomdir.
1015                do
1016                {
1017                        try
1018                        {
1019                                $rv = $this->_onView($legal_url, $jserr, $mime_filter, $list_type, $file_preselect_arg);
1020
1021                                if (!headers_sent()) header('Content-Type: application/json');
1022
1023                                echo json_encode(array_merge($rv['json'], array('files' => array_merge(array(), $rv['dirs'], $rv['files']))));
1024                                return;
1025                        }
1026                        catch(FileManagerException $e)
1027                        {
1028                                if ($emsg === null)
1029                                        $emsg = $e->getMessage();
1030                        }
1031                        catch(Exception $e)
1032                        {
1033                                // catching other severe failures; since this can be anything it may not be a translation keyword in the message...
1034                                if ($emsg === null)
1035                                        $emsg = $e->getMessage();
1036                        }
1037
1038                        // step down to the parent dir and retry:
1039                        $legal_url = self::getParentDir($legal_url);
1040                        $file_preselect_arg = null;
1041
1042                        $jserr['status']++;
1043
1044                } while ($legal_url !== false);
1045
1046                $this->modify_json4exception($jserr, $emsg . ' : path :: ' . $legal_url);
1047
1048                if (!headers_sent()) header('Content-Type: application/json');
1049
1050                // when we fail here, it's pretty darn bad and nothing to it.
1051                // just push the error JSON as go.
1052                echo json_encode($jserr);
1053        }
1054
1055        /**
1056         * Process the 'detail' event
1057         *
1058         * Returns a JSON encoded HTML chunk describing the specified file (metadata such
1059         * as size, format and possibly a thumbnail image as well)
1060         *
1061         * Expected parameters:
1062         *
1063         * $_POST['directory']     path relative to basedir a.k.a. options['directory'] root
1064         *
1065         * $_POST['file']          filename (including extension, of course) of the file to
1066         *                         be detailed.
1067         *
1068         * $_POST['filter']        optional mimetype filter string, amy be the part up to and
1069         *                         including the slash '/' or the full mimetype. Only files
1070         *                         matching this (set of) mimetypes will be listed.
1071         *                         Examples: 'image/' or 'application/zip'
1072         *
1073         * Errors will produce a JSON encoded error report, including at least two fields:
1074         *
1075         * status                  0 for error; nonzero for success
1076         *
1077         * error                   error message
1078         */
1079        protected function onDetail()
1080        {
1081                $emsg = null;
1082                $jserr = array(
1083                                'status' => 1
1084                        );
1085
1086                try
1087                {
1088                        $file_arg = $this->getPOSTparam('file');
1089                        if (empty($file_arg))
1090                                throw new FileManagerException('nofile');
1091
1092                        $dir_arg = $this->getPOSTparam('directory');
1093                        $legal_url = $this->rel2abs_legal_url_path($dir_arg);
1094                        $legal_url = self::enforceTrailingSlash($legal_url);
1095
1096                        $filename = pathinfo($file_arg, PATHINFO_BASENAME);
1097                        $legal_url .= $filename;
1098                        // must transform here so alias/etc. expansions inside legal_url_path2file_path() get a chance:
1099                        $file = $this->legal_url_path2file_path($legal_url);
1100
1101                        if (!is_readable($file))
1102                                throw new FileManagerException('nofile');
1103
1104                        $mime_filter = $this->getPOSTparam('filter', $this->options['filter']);
1105                        $mime_filters = $this->getAllowedMimeTypes($mime_filter);
1106                        $mime = $this->getMimeType($file);
1107                        if (is_file($file))
1108                        {
1109                                if (!$this->IsAllowedMimeType($mime, $mime_filters))
1110                                        throw new FileManagerException('extension');
1111                        }
1112                        else if (!is_dir($file))
1113                        {
1114                                throw new FileManagerException('nofile');
1115                        }
1116
1117                        $fileinfo = array(
1118                                        'legal_url' => $legal_url,
1119                                        'file' => $file,
1120                                        'filename' => $filename,
1121                                        'mime' => $mime,
1122                                        'mime_filter' => $mime_filter,
1123                                        'mime_filters' => $mime_filters
1124                                );
1125
1126                        if (!empty($this->options['DetailIsAuthorized_cb']) && function_exists($this->options['DetailIsAuthorized_cb']) && !$this->options['DetailIsAuthorized_cb']($this, 'detail', $fileinfo))
1127                                throw new FileManagerException('authorized');
1128
1129                        $legal_url = $fileinfo['legal_url'];
1130                        $file = $fileinfo['file'];
1131                        $filename = $fileinfo['filename'];
1132                        $mime = $fileinfo['mime'];
1133                        $mime_filter = $fileinfo['mime_filter'];
1134                        $mime_filters = $fileinfo['mime_filters'];
1135
1136                        $jserr = $this->extractDetailInfo($jserr, $legal_url, $file, $mime, $mime_filter);
1137
1138                        if (!headers_sent()) header('Content-Type: application/json');
1139
1140                        echo json_encode($jserr);
1141                        return;
1142                }
1143                catch(FileManagerException $e)
1144                {
1145                        $emsg = $e->getMessage();
1146                }
1147                catch(Exception $e)
1148                {
1149                        // catching other severe failures; since this can be anything and should only happen in the direst of circumstances, we don't bother translating
1150                        $emsg = $e->getMessage();
1151                }
1152
1153                $this->modify_json4exception($jserr, $emsg);
1154
1155                if (!headers_sent()) header('Content-Type: application/json');
1156
1157                // when we fail here, it's pretty darn bad and nothing to it.
1158                // just push the error JSON as go.
1159                echo json_encode($jserr);
1160        }
1161
1162        /**
1163         * Process the 'thumbnail' event
1164         *
1165         * Returns either the binary content of the requested thumbnail or the binary content of a replacement image.
1166         *
1167         * Technical info: this function is assumed to be fired from a <img src="..."> URI or similar and must produce
1168         * the content of an image.
1169         * It is used in conjection with the 'view/list=thumb' view mode of the FM client: the 'view' list, as
1170         * produced by us, contains specially crafted URLs pointing back at us (the 'event=thumbnail' URLs) to
1171         * enable FM to cope much better with large image collections by having the entire thumbnail checking
1172         * and creation process offloaded to this Just-in-Time subevent.
1173         *
1174         * By not loading the 'view' event with the thumbnail precreation/checking effort, it can respond
1175         * much faster or at least not timeout in the backend for larger image sets in any directory.
1176         * ('view' simply assumes the thumbnail will be there, hence reducing its own workload with at least
1177         * 1 file_exists() plus worst-case one GD imageinfo + imageresample + extras per image in the 'view' list!)
1178         *
1179         * Expected parameters:
1180         *
1181         * $_GET['directory']      path relative to basedir a.k.a. options['directory'] root
1182         *
1183         * $_GET['file']           filename (including extension, of course) of the file to
1184         *                         be thumbnailed.
1185         *
1186         * $_GET['size']           the requested thumbnail maximum width / height (the bounding box is square).
1187         *                         Must be one of our 'authorized' sizes: 48, 250.
1188         *
1189         * $_GET['filter']         optional mimetype filter string, amy be the part up to and
1190         *                         including the slash '/' or the full mimetype. Only files
1191         *                         matching this (set of) mimetypes will be listed.
1192         *                         Examples: 'image/' or 'application/zip'
1193         *
1194         * $_GET['asJson']        return some JSON {status: 1, thumbnail: 'path/to/thumbnail.png' }
1195         *
1196         * Errors will produce a JSON encoded error report, including at least two fields:
1197         *
1198         * status                  0 for error; nonzero for success
1199         *
1200         * error                   error message
1201         *
1202         * Next to these, the JSON encoded output will, with high probability, include a
1203         * list view of the parent or 'basedir' as a fast and easy fallback mechanism for client side
1204         * viewing code. However, severe and repetitive errors may not produce this
1205         * 'fallback view list' so proper client code should check the 'status' field in the
1206         * JSON output.
1207         */
1208        protected function onThumbnail($GET = NULL)
1209        {
1210                // try to produce the view; if it b0rks, retry with the parent, until we've arrived at the basedir:
1211                // then we fail more severely.
1212
1213                $emsg = null;
1214                $img_filepath = null;
1215                $reqd_size = 48;
1216                $filename = null;
1217
1218                try
1219                {
1220                        $reqd_size = isset($GET) ? @$GET['size'] : intval($this->getGETparam('size'));
1221                        if (empty($reqd_size))
1222                                throw new FileManagerException('disabled');
1223                        // and when not requesting one of our 'authorized' thumbnail sizes, you're gonna burn as well!
1224                        if (!in_array($reqd_size, array(16, 48, 250)))
1225                                throw new FileManagerException('disabled');
1226
1227                        $file_arg = isset($GET) ? @$GET['file'] :$this->getGETparam('file');
1228                        if (empty($file_arg))
1229                                throw new FileManagerException('nofile');
1230
1231                        $dir_arg = isset($GET) ? @$GET['directory'] :$this->getGETparam('directory');
1232                        $legal_url = $this->rel2abs_legal_url_path($dir_arg);
1233                        $legal_url = self::enforceTrailingSlash($legal_url);
1234
1235                        $filename = pathinfo($file_arg, PATHINFO_BASENAME);
1236                        $legal_url .= $filename;
1237                        // must transform here so alias/etc. expansions inside legal_url_path2file_path() get a chance:
1238                        $file = $this->legal_url_path2file_path($legal_url);
1239
1240                        if (!is_readable($file))
1241                                throw new FileManagerException('nofile');
1242
1243                        $mime_filter = isset($GET) ? (isset($GET['filter']) ? $GET['filter'] : $this->options['filter']) :$this->getGETparam('filter', $this->options['filter']);
1244                        $mime_filters = $this->getAllowedMimeTypes($mime_filter);
1245                        $mime = $this->getMimeType($file);
1246                        if (is_file($file))
1247                        {
1248                                if (!$this->IsAllowedMimeType($mime, $mime_filters))
1249                                        throw new FileManagerException('extension');
1250                        }
1251                        else
1252                        {
1253                                throw new FileManagerException('nofile');
1254                        }
1255
1256                        $fileinfo = array(
1257                                        'legal_url' => $legal_url,
1258                                        'file' => $file,
1259                                        'filename' => $filename,
1260                                        'mime' => $mime,
1261                                        'mime_filter' => $mime_filter,
1262                                        'mime_filters' => $mime_filters,
1263                                        'requested_size' => $reqd_size
1264                                );
1265
1266                        if (!empty($this->options['ThumbnailIsAuthorized_cb']) && function_exists($this->options['ThumbnailIsAuthorized_cb']) && !$this->options['ThumbnailIsAuthorized_cb']($this, 'thumbnail', $fileinfo))
1267                                throw new FileManagerException('authorized');
1268
1269                        $legal_url = $fileinfo['legal_url'];
1270                        $file = $fileinfo['file'];
1271                        $filename = $fileinfo['filename'];
1272                        $mime = $fileinfo['mime'];
1273                        $mime_filter = $fileinfo['mime_filter'];
1274                        $mime_filters = $fileinfo['mime_filters'];
1275                        $reqd_size = $fileinfo['requested_size'];
1276
1277                        /*
1278                         * each image we inspect may throw an exception due to a out of memory warning
1279                         * (which is far better than without those: a silent fatal abort!)
1280                         *
1281                         * However, now that we do have a way to check most memory failures occurring in here (due to large images
1282                         * and too little available RAM) we /still/ want to see that happen: for broken and overlarge images, we
1283                         * produce some alternative graphics instead!
1284                         */
1285                        $thumb_path = null;
1286                        if (FileManagerUtility::startsWith($mime, 'image/'))
1287                        {
1288                                // access the image and create a thumbnail image; this can fail dramatically
1289                                $thumb_path = $this->getThumb($legal_url, $file, $reqd_size, $reqd_size);
1290                        }
1291
1292                        $img_filepath = (!empty($thumb_path) ? $thumb_path : $this->getIcon($filename, $reqd_size <= 16));
1293                }
1294                catch(FileManagerException $e)
1295                {
1296                        $emsg = $e->getMessage();
1297                }
1298                catch(Exception $e)
1299                {
1300                        // catching other severe failures; since this can be anything and should only happen in the direst of circumstances, we don't bother translating
1301                        $emsg = $e->getMessage();
1302                }
1303
1304                // now go and serve the content of the thumbnail / icon image file (which we still need to determine /exactly/):
1305                try
1306                {
1307                        if (empty($img_filepath))
1308                        {
1309                                $img_filepath = $this->getIconForError($emsg, $filename, $reqd_size <= 16);
1310                        }
1311     
1312      if(isset($GET))
1313      {
1314        return $img_filepath;
1315      }
1316     
1317      if($this->getGETParam('asJson', 0))
1318      {
1319        $Response = array('status' => 1, 'thumbnail' => $img_filepath);
1320        if(@$emsg)
1321        {
1322          $Response['status'] = 0;
1323          $Response['error']  = $emsg;
1324        }       
1325       
1326        if (!headers_sent()) header('Content-Type: application/json');
1327        echo json_encode($Response);
1328        return;
1329      }
1330
1331                        $file = $this->url_path2file_path($img_filepath);
1332                        $mime = $this->getMimeType($file);
1333                        $fd = fopen($file, 'rb');
1334                        if (!$fd)
1335                        {
1336                                // when the icon / thumbnail cannot be opened for whatever reason, fall back to the default error image:
1337                                $file = $this->url_path2file_path($this->getIcon('is.default-error', $reqd_size <= 16));
1338                                $mime = $this->getMimeType($file);
1339                                $fd = fopen($file, 'rb');
1340                                if (!$fd)
1341                                        throw new Exception('panic');
1342                        }
1343                        $fsize = filesize($file);
1344                        if (!empty($mime))
1345                        {
1346                                header('Content-Type: ' . $mime);
1347                        }
1348                        header('Content-Length: ' . $fsize);
1349
1350                        header("Cache-Control: private"); //use this to open files directly
1351
1352                        fpassthru($fd);
1353                        fclose($fd);
1354                        exit();
1355                }
1356                catch(Exception $e)
1357                {
1358                        send_response_status_header(500);
1359                        echo 'Cannot produce thumbnail: ' . $emsg . ' :: ' . $img_filepath;
1360                }
1361        }
1362
1363
1364        /**
1365         * Process the 'destroy' event
1366         *
1367         * Delete the specified file or directory and return a JSON encoded status of success
1368         * or failure.
1369         *
1370         * Note that when images are deleted, so are their thumbnails.
1371         *
1372         * Expected parameters:
1373         *
1374         * $_POST['directory']     path relative to basedir a.k.a. options['directory'] root
1375         *
1376         * $_POST['file']          filename (including extension, of course) of the file to
1377         *                         be detailed.
1378         *
1379         * $_POST['filter']        optional mimetype filter string, amy be the part up to and
1380         *                         including the slash '/' or the full mimetype. Only files
1381         *                         matching this (set of) mimetypes will be listed.
1382         *                         Examples: 'image/' or 'application/zip'
1383         *
1384         * Errors will produce a JSON encoded error report, including at least two fields:
1385         *
1386         * status                  0 for error; nonzero for success
1387         *
1388         * error                   error message
1389         */
1390        protected function onDestroy()
1391        {
1392                $emsg = null;
1393                $jserr = array(
1394                                'status' => 1
1395                        );
1396
1397                try
1398                {
1399                        if (!$this->options['destroy'])
1400                                throw new FileManagerException('disabled');
1401
1402                        $file_arg = $this->getPOSTparam('file');
1403                        if (empty($file_arg))
1404                                throw new FileManagerException('nofile');
1405
1406                        $dir_arg = $this->getPOSTparam('directory');
1407                        $legal_url = $this->rel2abs_legal_url_path($dir_arg);
1408                        $legal_url = self::enforceTrailingSlash($legal_url);
1409
1410                        $filename = pathinfo($file_arg, PATHINFO_BASENAME);
1411                        $legal_url .= $filename;
1412                        // must transform here so alias/etc. expansions inside legal_url_path2file_path() get a chance:
1413                        $file = $this->legal_url_path2file_path($legal_url);
1414
1415                        if (!file_exists($file))
1416                                throw new FileManagerException('nofile');
1417
1418                        $mime_filter = $this->getPOSTparam('filter', $this->options['filter']);
1419                        $mime = $this->getMimeType($file);
1420                        $mime_filters = $this->getAllowedMimeTypes($mime_filter);
1421                        if (is_file($file))
1422                        {
1423                                if (!$this->IsAllowedMimeType($mime, $mime_filters))
1424                                        throw new FileManagerException('extension');
1425                        }
1426                        else if (!is_dir($file))
1427                        {
1428                                throw new FileManagerException('nofile');
1429                        }
1430
1431                        $fileinfo = array(
1432                                        'legal_url' => $legal_url,
1433                                        'file' => $file,
1434                                        'mime' => $mime,
1435                                        'mime_filter' => $mime_filter,
1436                                        'mime_filters' => $mime_filters
1437                                );
1438
1439                        if (!empty($this->options['DestroyIsAuthorized_cb']) && function_exists($this->options['DestroyIsAuthorized_cb']) && !$this->options['DestroyIsAuthorized_cb']($this, 'destroy', $fileinfo))
1440                                throw new FileManagerException('authorized');
1441
1442                        $legal_url = $fileinfo['legal_url'];
1443                        $file = $fileinfo['file'];
1444                        $mime = $fileinfo['mime'];
1445                        $mime_filter = $fileinfo['mime_filter'];
1446                        $mime_filters = $fileinfo['mime_filters'];
1447
1448                        if (!$this->unlink($legal_url, $mime_filters))
1449                                throw new FileManagerException('unlink_failed:' . $legal_url);
1450
1451                        if (!headers_sent()) header('Content-Type: application/json');
1452
1453                        echo json_encode(array(
1454                                        'status' => 1,
1455                                        'content' => 'destroyed'
1456                                ));
1457                        return;
1458                }
1459                catch(FileManagerException $e)
1460                {
1461                        $emsg = $e->getMessage();
1462                }
1463                catch(Exception $e)
1464                {
1465                        // catching other severe failures; since this can be anything and should only happen in the direst of circumstances, we don't bother translating
1466                        $emsg = $e->getMessage();
1467                }
1468
1469                $this->modify_json4exception($jserr, $emsg);
1470
1471                if (!headers_sent()) header('Content-Type: application/json');
1472
1473                // when we fail here, it's pretty darn bad and nothing to it.
1474                // just push the error JSON as go.
1475                echo json_encode($jserr);
1476        }
1477
1478        /**
1479         * Process the 'create' event
1480         *
1481         * Create the specified subdirectory and give it the configured permissions
1482         * (options['chmod'], default 0777) and return a JSON encoded status of success
1483         * or failure.
1484         *
1485         * Expected parameters:
1486         *
1487         * $_POST['directory']     path relative to basedir a.k.a. options['directory'] root
1488         *
1489         * $_POST['file']          name of the subdirectory to be created
1490         *
1491         * Extra input parameters considered while producing the JSON encoded directory view.
1492         * These may not seem relevant for an empty directory, but these parameters are also
1493         * considered when providing the fallback directory view in case an error occurred
1494         * and then the listed directory (either the parent or the basedir itself) may very
1495         * likely not be empty!
1496         *
1497         * $_POST['filter']        optional mimetype filter string, amy be the part up to and
1498         *                         including the slash '/' or the full mimetype. Only files
1499         *                         matching this (set of) mimetypes will be listed.
1500         *                         Examples: 'image/' or 'application/zip'
1501         *
1502         * $_POST['type']          'thumb' will produce a list view including thumbnail and other
1503         *                         information with each listed file; other values will produce
1504         *                         a basic list view (similar to Windows Explorer 'list' view).
1505         *
1506         * Errors will produce a JSON encoded error report, including at least two fields:
1507         *
1508         * status                  0 for error; nonzero for success
1509         *
1510         * error                   error message
1511         */
1512        protected function onCreate()
1513        {
1514                $emsg = null;
1515                $jserr = array(
1516                                'status' => 1
1517                        );
1518
1519                $mime_filter = $this->getPOSTparam('filter', $this->options['filter']);
1520                $list_type = ($this->getPOSTparam('type') != 'thumb' ? 'list' : 'thumb');
1521
1522                $legal_url = null;
1523
1524                try
1525                {
1526                        $dir_arg = $this->getPOSTparam('directory');
1527                        $legal_url = $this->rel2abs_legal_url_path($dir_arg);
1528                        $legal_url = self::enforceTrailingSlash($legal_url);
1529
1530                        if (!$this->options['create'])
1531                                throw new FileManagerException('disabled');
1532
1533                        $file_arg = $this->getPOSTparam('file');
1534                        if (empty($file_arg))
1535                                throw new FileManagerException('nofile');
1536
1537                        $filename = pathinfo($file_arg, PATHINFO_BASENAME);
1538                        //$legal_url .= $filename;
1539                        // must transform here so alias/etc. expansions inside legal_url_path2file_path() get a chance:
1540                        $dir = $this->legal_url_path2file_path($legal_url);
1541
1542                        if (!is_dir($dir))
1543                                throw new FileManagerException('nofile');
1544
1545                        $file = $this->getUniqueName(array('filename' => $filename), $dir);  // a directory has no 'extension'!
1546                        if (!$file)
1547                                throw new FileManagerException('nofile');
1548                        $newdir = $this->legal_url_path2file_path($legal_url . $file);
1549
1550                        $fileinfo = array(
1551                                        'legal_url' => $legal_url,
1552                                        'dir' => $dir,
1553                                        'raw_name' => $filename,
1554                                        'uniq_name' => $file,
1555                                        'newdir' => $newdir,
1556                                        'chmod' => $this->options['chmod']
1557                                );
1558                        if (!empty($this->options['CreateIsAuthorized_cb']) && function_exists($this->options['CreateIsAuthorized_cb']) && !$this->options['CreateIsAuthorized_cb']($this, 'create', $fileinfo))
1559                                throw new FileManagerException('authorized');
1560
1561                        $legal_url = $fileinfo['legal_url'];
1562                        $dir = $fileinfo['dir'];
1563                        $filename = $fileinfo['raw_name'];
1564                        $file = $fileinfo['uniq_name'];
1565                        $newdir = $fileinfo['newdir'];
1566
1567                        if (!@mkdir($newdir, $fileinfo['chmod'], true))
1568                                throw new FileManagerException('mkdir_failed:' . $this->legal2abs_url_path($legal_url) . $file);
1569
1570                        if (!headers_sent()) header('Content-Type: application/json');
1571
1572                        // success, now show the new directory as a list view:
1573                        $rv = $this->_onView($legal_url . $file . '/', $jserr, $mime_filter, $list_type);
1574                        echo json_encode(array_merge($rv['json'], array('files' => array_merge(array(), $rv['dirs'], $rv['files']))));
1575                        return;
1576                }
1577                catch(FileManagerException $e)
1578                {
1579                        $emsg = $e->getMessage();
1580
1581                        $jserr['status'] = 0;
1582
1583                        // and fall back to showing the PARENT directory
1584                        try
1585                        {
1586                                $rv = $this->_onView($legal_url, $jserr, $mime_filter, $list_type);
1587                                $jserr = array_merge($rv['json'], array('files' => array_merge(array(), $rv['dirs'], $rv['files'])));
1588                        }
1589                        catch (Exception $e)
1590                        {
1591                                // and fall back to showing the BASEDIR directory
1592                                try
1593                                {
1594                                        $legal_url = $this->options['directory'];
1595                                        $rv = $this->_onView($legal_url, $jserr, $mime_filter, $list_type);
1596                                        $jserr = array_merge($rv['json'], array('files' => array_merge(array(), $rv['dirs'], $rv['files'])));
1597                                }
1598                                catch (Exception $e)
1599                                {
1600                                        // when we fail here, it's pretty darn bad and nothing to it.
1601                                        // just push the error JSON as go.
1602                                }
1603                        }
1604                }
1605                catch(Exception $e)
1606                {
1607                        // catching other severe failures; since this can be anything and should only happen in the direst of circumstances, we don't bother translating
1608                        $emsg = $e->getMessage();
1609
1610                        $jserr['status'] = 0;
1611
1612                        // and fall back to showing the PARENT directory
1613                        try
1614                        {
1615                                $rv = $this->_onView($legal_url, $jserr, $mime_filter, $list_type);
1616                                $jserr = array_merge($rv['json'], array('files' => array_merge(array(), $rv['dirs'], $rv['files'])));
1617                        }
1618                        catch (Exception $e)
1619                        {
1620                                // and fall back to showing the BASEDIR directory
1621                                try
1622                                {
1623                                        $legal_url = $this->options['directory'];
1624                                        $rv = $this->_onView($legal_url, $jserr, $mime_filter, $list_type);
1625                                        $jserr = array_merge($rv['json'], array('files' => array_merge(array(), $rv['dirs'], $rv['files'])));
1626                                }
1627                                catch (Exception $e)
1628                                {
1629                                        // when we fail here, it's pretty darn bad and nothing to it.
1630                                        // just push the error JSON as go.
1631                                }
1632                        }
1633                }
1634
1635                $this->modify_json4exception($jserr, $emsg);
1636
1637                if (!headers_sent()) header('Content-Type: application/json');
1638
1639                // when we fail here, it's pretty darn bad and nothing to it.
1640                // just push the error JSON as go.
1641                echo json_encode($jserr);
1642        }
1643
1644        /**
1645         * Process the 'download' event
1646         *
1647         * Send the file content of the specified file for download by the client.
1648         * Only files residing within the directory tree rooted by the
1649         * 'basedir' (options['directory']) will be allowed to be downloaded.
1650         *
1651         * Expected parameters:
1652         *
1653         * $_GET['file']          filepath of the file to be downloaded
1654         *
1655         * $_GET['filter']        optional mimetype filter string, amy be the part up to and
1656         *                        including the slash '/' or the full mimetype. Only files
1657         *                        matching this (set of) mimetypes will be listed.
1658         *                        Examples: 'image/' or 'application/zip'
1659         *
1660         * $_GET['asJson']        if true, returns { status:1, url: '.....' } or { status:0, error: '......' }
1661         *
1662         * On errors a HTTP 403 error response will be sent instead.
1663         */
1664        protected function onDownload()
1665        {
1666                try
1667                {
1668                        if (!$this->options['download'])
1669                                throw new FileManagerException('disabled');
1670
1671                        $file_arg = $this->getGETparam('file');
1672                        if (empty($file_arg))
1673                                throw new FileManagerException('nofile');
1674
1675                        $legal_url = $this->rel2abs_legal_url_path($file_arg);
1676                        //$legal_url = self::enforceTrailingSlash($legal_url);
1677
1678                        // must transform here so alias/etc. expansions inside legal_url_path2file_path() get a chance:
1679                        $file = $this->legal_url_path2file_path($legal_url);
1680
1681                        if (!is_readable($file))
1682                                throw new FileManagerException('nofile');
1683
1684                        $mime_filter = $this->getGETparam('filter', $this->options['filter']);
1685                        $mime = $this->getMimeType($file);
1686                        $mime_filters = $this->getAllowedMimeTypes($mime_filter);
1687                        if (is_file($file))
1688                        {
1689                                if (!$this->IsAllowedMimeType($mime, $mime_filters))
1690                                        throw new FileManagerException('extension');
1691                        }
1692                        else
1693                        {
1694                                throw new FileManagerException('nofile');
1695                        }
1696
1697                        $fileinfo = array(
1698                                        'legal_url' => $legal_url,
1699                                        'file' => $file,
1700                                        'mime' => $mime,
1701                                        'mime_filter' => $mime_filter,
1702                                        'mime_filters' => $mime_filters
1703                                );
1704                        if (!empty($this->options['DownloadIsAuthorized_cb']) && function_exists($this->options['DownloadIsAuthorized_cb']) && !$this->options['DownloadIsAuthorized_cb']($this, 'download', $fileinfo))
1705                                throw new FileManagerException('authorized');
1706
1707      if($this->getGETparam('asJson', 0))
1708      {
1709        $Response = array('status' => 1, 'url' => $this->legal2abs_url_path($fileinfo['legal_url']), 'fileinfo' => $fileinfo);
1710        echo json_encode($Response);
1711        exit;
1712      }
1713     
1714                        $legal_url = $fileinfo['legal_url'];
1715                        $file = $fileinfo['file'];
1716                        $mime = $fileinfo['mime'];
1717                        $mime_filter = $fileinfo['mime_filter'];
1718                        $mime_filters = $fileinfo['mime_filters'];
1719
1720                        if ($fd = fopen($file, 'rb'))
1721                        {
1722                                $fsize = filesize($file);
1723                                $path_parts = pathinfo($legal_url);
1724                                $ext = strtolower($path_parts["extension"]);
1725                                switch ($ext)
1726                                {
1727                                case "pdf":
1728                                        header('Content-Type: application/pdf');
1729                                        header('Content-Disposition: attachment; filename="' . $path_parts["basename"] . '"'); // use 'attachment' to force a download
1730                                        break;
1731
1732                                // add here more headers for diff. extensions
1733
1734                                default;
1735                                        header('Content-Type: application/octet-stream');
1736                                        header('Content-Disposition: filename="' . $path_parts["basename"] . '"');
1737                                        break;
1738                                }
1739                                header("Content-length: $fsize");
1740                                header("Cache-control: private"); //use this to open files directly
1741
1742                                fpassthru($fd);
1743                                fclose($fd);
1744                        }
1745                }
1746                catch(FileManagerException $e)
1747                {
1748      if($this->getGETparam('asJson', 0))
1749      {
1750        $Response = array('status' => 0, 'error' => $e->getMessage());
1751        echo json_encode($Response);
1752        exit;
1753      }
1754     
1755                        // we don't care whether it's a 404, a 403 or something else entirely: we feed 'em a 403 and that's final!
1756                        send_response_status_header(403);
1757                        echo $e->getMessage();
1758                }
1759                catch(Exception $e)
1760                {
1761      if($this->getGETparam('asJson', 0))
1762      {
1763        $Response = array('status' => 0, 'error' => $e->getMessage());
1764        echo json_encode($Response);
1765        exit;
1766      }
1767     
1768                        // we don't care whether it's a 404, a 403 or something else entirely: we feed 'em a 403 and that's final!
1769                        send_response_status_header(403);
1770                        echo $e->getMessage();
1771                }
1772        }
1773
1774        /**
1775         * Process the 'upload' event
1776         *
1777         * Process and store the uploaded file in the designated location.
1778         * Images will be resized when possible and applicable. A thumbnail image will also
1779         * be preproduced when possible.
1780         * Return a JSON encoded status of success or failure.
1781         *
1782         * Expected parameters:
1783         *
1784         * $_GET['directory']     path relative to basedir a.k.a. options['directory'] root
1785         *
1786         * $_GET['resize']        nonzero value indicates any uploaded image should be resized to the configured options['maxImageDimension'] width and height whenever possible
1787         *
1788         * $_GET['filter']        optional mimetype filter string, amy be the part up to and
1789         *                        including the slash '/' or the full mimetype. Only files
1790         *                        matching this (set of) mimetypes will be listed.
1791         *                        Examples: 'image/' or 'application/zip'
1792         *
1793         * $_FILES[]              the metadata for the uploaded file
1794         *
1795         * Errors will produce a JSON encoded error report, including at least two fields:
1796         *
1797         * status                  0 for error; nonzero for success
1798         *
1799         * error                   error message
1800         */
1801        protected function onUpload()
1802        {
1803                $emsg = null;
1804                $jserr = array(
1805                                'status' => 1
1806                        );
1807
1808                try
1809                {
1810                        if (!$this->options['upload'])
1811                                throw new FileManagerException('disabled');
1812
1813                        if (!isset($_FILES) || empty($_FILES['Filedata']) || empty($_FILES['Filedata']['name']) || empty($_FILES['Filedata']['size']))
1814                                throw new FileManagerException('nofile');
1815
1816                        $file_arg = $_FILES['Filedata']['name'];
1817                        if (empty($file_arg))
1818                                throw new FileManagerException('nofile');
1819
1820                        $dir_arg = $this->getPOSTparam('directory');
1821                        $legal_url = $this->rel2abs_legal_url_path($dir_arg);
1822                        $legal_url = self::enforceTrailingSlash($legal_url);
1823                        // must transform here so alias/etc. expansions inside legal_url_path2file_path() get a chance:
1824                        $dir = $this->legal_url_path2file_path($legal_url);
1825
1826                        $filename = $this->getUniqueName($file_arg, $dir);
1827                        if (!$filename)
1828                                throw new FileManagerException('nofile');
1829                        $fi = pathinfo($filename);
1830
1831
1832                        $mime_filter = $this->getGETparam('filter', $this->options['filter']);
1833                        $tmppath = $_FILES['Filedata']['tmp_name'];
1834                       
1835                        // tmp_name does not include an extention, if we don't provide it, the mime detection is too
1836                        // easily thwarted and returns application/octet-stream, if you are using a filter to only have images, you're dead
1837                        // even if it was an image (because the GD based image check only checks if the extention is recognised)                       
1838                        $mime = $this->getMimeType($tmppath, false, strtolower(preg_replace('/.*\.([a-z0-9]+)$/i', '$1',$_FILES['Filedata']['name'])) );
1839                        $mime_filters = $this->getAllowedMimeTypes($mime_filter);
1840                        if (!$this->IsAllowedMimeType($mime, $mime_filters))
1841                                throw new FileManagerException('extension');
1842
1843                        /*
1844                        Security:
1845
1846                        Upload::move() processes the unfiltered version of $_FILES[]['name'], at least to get the extension,
1847                        unless we ALWAYS override the filename and extension in the options array below. That's why we
1848                        calculate the extension at all times here.
1849                        */
1850                        if (!is_string($fi['extension']) || strlen($fi['extension']) == 0) // can't use 'empty()' as "0" is a valid extension itself.
1851                        {
1852                                //enforce a mandatory extension, even when there isn't one (due to filtering or original input producing none)
1853                                $fi['extension'] = 'txt';
1854                        }
1855                        else if ($this->options['safe'] && in_array(strtolower($fi['extension']), array('exe', 'dll', 'com', 'php', 'php3', 'php4', 'php5', 'phps')))
1856                        {
1857                                $fi['extension'] = 'txt';
1858                        }
1859
1860                        $fileinfo = array(
1861                                'legal_url' => $legal_url,
1862                                'dir' => $dir,
1863                                'raw_filename' => $file_arg,
1864                                'name' => $fi['filename'],
1865                                'extension' => $fi['extension'],
1866                                'mime' => $mime,
1867                                'mime_filter' => $mime_filter,
1868                                'mime_filters' => $mime_filters,
1869                                'tmp_filepath' => $tmppath,
1870                                'size' => $_FILES['Filedata']['size'],
1871                                'maxsize' => $this->options['maxUploadSize'],
1872                                'overwrite' => false,
1873                                'chmod' => $this->options['chmod'] & 0666   // security: never make those files 'executable'!
1874                        );
1875                        if (!empty($this->options['UploadIsAuthorized_cb']) && function_exists($this->options['UploadIsAuthorized_cb']) && !$this->options['UploadIsAuthorized_cb']($this, 'upload', $fileinfo))
1876                                throw new FileManagerException('authorized');
1877
1878                        $legal_url = $fileinfo['legal_url'];
1879                        $dir = $fileinfo['dir'];
1880                        $file_arg = $fileinfo['raw_filename'];
1881                        $filename = $fileinfo['name'] . (!empty($fileinfo['extension']) ? '.' . $fileinfo['extension'] : '');
1882                        $mime = $fileinfo['mime'];
1883                        $mime_filter = $fileinfo['mime_filter'];
1884                        $mime_filters = $fileinfo['mime_filters'];
1885                        //$tmppath = $fileinfo['tmp_filepath'];
1886
1887                        if($fileinfo['maxsize'] && $fileinfo['size'] > $fileinfo['maxsize'])
1888                                throw new FileManagerException('size');
1889
1890                        if(!$fileinfo['extension'])
1891                                throw new FileManagerException('extension');
1892
1893                        // must transform here so alias/etc. expansions inside legal_url_path2file_path() get a chance:
1894                        $file = $this->legal_url_path2file_path($legal_url . $filename);
1895
1896                        if(!$fileinfo['overwrite'] && file_exists($file))
1897                                throw new FileManagerException('exists');
1898
1899                        if(!move_uploaded_file($_FILES['Filedata']['tmp_name'], $file))
1900                                throw new FileManagerException(strtolower($_FILES['Filedata']['error'] <= 2 ? 'size' : ($_FILES['Filedata']['error'] == 3 ? 'partial' : 'path')));
1901
1902                        @chmod($file, $fileinfo['chmod']);
1903
1904
1905                        /*
1906                         * NOTE: you /can/ (and should be able to, IMHO) upload 'overly large' image files to your site, but the resizing process step
1907                         *       happening here will fail; we have memory usage estimators in place to make the fatal crash a non-silent one, i,e, one
1908                         *       where we still have a very high probability of NOT fatally crashing the PHP iunterpreter but catching a suitable exception
1909                         *       instead.
1910                         *       Having uploaded such huge images, a developer/somebody can always go in later and up the memory limit if the site admins
1911                         *       feel it is deserved. Until then, no thumbnails of such images (though you /should/ be able to milkbox-view the real thing!)
1912                         */
1913                        if (FileManagerUtility::startsWith($mime, 'image/') && $this->getGETparam('resize', 0))
1914                        {
1915                                $img = new Image($file);
1916                                $size = $img->getSize();
1917                                // Image::resize() takes care to maintain the proper aspect ratio, so this is easy
1918                                // (default quality is 100% for JPEG so we get the cleanest resized images here)
1919                                $img->resize($this->options['maxImageDimension']['width'], $this->options['maxImageDimension']['height'])->save();
1920                                unset($img);
1921                        }
1922
1923                        if (!headers_sent()) header('Content-Type: text/plain');//application/json');
1924
1925                        echo json_encode(array(
1926                                        'status' => 1,
1927                                        'name' => pathinfo($file, PATHINFO_BASENAME)
1928                                ));
1929                        return;
1930                }
1931                catch(FileManagerException $e)
1932                {
1933                        $emsg = $e->getMessage();
1934                }
1935                catch(Exception $e)
1936                {
1937                        // catching other severe failures; since this can be anything and should only happen in the direst of circumstances, we don't bother translating
1938                        $emsg = $e->getMessage();
1939                }
1940
1941                $this->modify_json4exception($jserr, $emsg);
1942
1943                if (!headers_sent()) header('Content-Type: text/plain');//application/json');
1944
1945                // when we fail here, it's pretty darn bad and nothing to it.
1946                // just push the error JSON as go.
1947                echo json_encode(array_merge($jserr, $_FILES));
1948        }
1949
1950        /**
1951         * Process the 'move' event (with is used by both move/copy and rename client side actions)
1952         *
1953         * Copy or move/rename a given file or directory and return a JSON encoded status of success
1954         * or failure.
1955         *
1956         * Expected parameters:
1957         *
1958         * $_POST['copy']            nonzero value means copy, zero or nil for move/rename
1959         *
1960         * Source filespec:
1961         *
1962         *   $_POST['directory']     path relative to basedir a.k.a. options['directory'] root
1963         *
1964         *   $_POST['file']          original name of the file/subdirectory to be renamed/copied
1965         *
1966         * Destination filespec:
1967         *
1968         *   $_POST['newDirectory']  path relative to basedir a.k.a. options['directory'] root;
1969         *                           target directory where the file must be moved / copied
1970         *
1971         *   $_POST['name']          target name of the file/subdirectory to be renamed
1972         *
1973         * Errors will produce a JSON encoded error report, including at least two fields:
1974         *
1975         * status                    0 for error; nonzero for success
1976         *
1977         * error                     error message
1978         */
1979        protected function onMove()
1980        {
1981                $emsg = null;
1982                $jserr = array(
1983                                'status' => 1
1984                        );
1985
1986                try
1987                {
1988                        if (!$this->options['move'])
1989                                throw new FileManagerException('disabled');
1990
1991                        $file_arg = $this->getPOSTparam('file');
1992                        if (empty($file_arg))
1993                                throw new FileManagerException('nofile');
1994
1995                        $dir_arg = $this->getPOSTparam('directory');
1996                        $legal_url = $this->rel2abs_legal_url_path($dir_arg);
1997                        $legal_url = self::enforceTrailingSlash($legal_url);
1998
1999                        $filename = pathinfo($file_arg, PATHINFO_BASENAME);
2000                        //$legal_url .= $filename;
2001                        // must transform here so alias/etc. expansions inside legal_url_path2file_path() get a chance:
2002                        $dir = $this->legal_url_path2file_path($legal_url);
2003                        $path = $this->legal_url_path2file_path($legal_url . $filename);
2004
2005                        if (!file_exists($path))
2006                                throw new FileManagerException('nofile');
2007
2008                        $is_dir = is_dir($path);
2009
2010                        $newdir_arg = $this->getPOSTparam('newDirectory');
2011                        $newname_arg = $this->getPOSTparam('name');
2012                        $rename = (empty($newdir_arg) && !empty($newname_arg));
2013
2014                        $is_copy = !!$this->getPOSTparam('copy');
2015
2016
2017                        // note: we do not support copying entire directories, though directory rename/move is okay
2018                        if ($is_copy && $is_dir)
2019                                throw new FileManagerException('disabled');
2020
2021                        if($rename)
2022                        {
2023                                $fn = 'rename';
2024                                $legal_newurl = $legal_url;
2025                                $newdir = $dir;
2026
2027                                $newname = pathinfo($newname_arg, PATHINFO_BASENAME);
2028                                if ($is_dir)
2029                                        $newname = $this->getUniqueName(array('filename' => $newname), $newdir);  // a directory has no 'extension'
2030                                else
2031                                        $newname = $this->getUniqueName($newname, $newdir);
2032
2033                                if (!$newname)
2034                                        throw new FileManagerException('nonewfile');
2035
2036                                // when the new name seems to have a different extension, make sure the extension doesn't change after all:
2037                                // Note: - if it's only 'case' we're changing here, then exchange the extension instead of appending it.
2038                                //       - directories do not have extensions
2039                                $extOld = pathinfo($filename, PATHINFO_EXTENSION);
2040                                $extNew = pathinfo($newname, PATHINFO_EXTENSION);
2041                                if ((!$this->options['allowExtChange'] || (!$is_dir && empty($extNew))) && !empty($extOld) && strtolower($extOld) != strtolower($extNew))
2042                                {
2043                                        $newname .= '.' . $extOld;
2044                                }
2045                        }
2046                        else
2047                        {
2048                                $fn = ($is_copy ? 'copy' : 'rename' /* 'move' */);
2049                                $legal_newurl = $this->rel2abs_legal_url_path($newdir_arg);
2050                                $legal_newurl = self::enforceTrailingSlash($legal_newurl);
2051                                $newdir = $this->legal_url_path2file_path($legal_newurl);
2052
2053                                if ($is_dir)
2054                                        $newname = $this->getUniqueName(array('filename' => $filename), $newdir);  // a directory has no 'extension'
2055                                else
2056                                        $newname = $this->getUniqueName($filename, $newdir);
2057
2058                                if (!$newname)
2059                                        throw new FileManagerException('nonewfile');
2060                        }
2061
2062                        $newpath = $this->legal_url_path2file_path($legal_newurl . $newname);
2063
2064
2065                        $fileinfo = array(
2066                                        'legal_url' => $legal_url,
2067                                        'dir' => $dir,
2068                                        'path' => $path,
2069                                        'name' => $filename,
2070                                        'legal_newurl' => $legal_newurl,
2071                                        'newdir' => $newdir,
2072                                        'newpath' => $newpath,
2073                                        'newname' => $newname,
2074                                        'rename' => $rename,
2075                                        'is_dir' => $is_dir,
2076                                        'function' => $fn
2077                                );
2078
2079                        if (!empty($this->options['MoveIsAuthorized_cb']) && function_exists($this->options['MoveIsAuthorized_cb']) && !$this->options['MoveIsAuthorized_cb']($this, 'move', $fileinfo))
2080                                throw new FileManagerException('authorized');
2081
2082                        $legal_url = $fileinfo['legal_url'];
2083                        $dir = $fileinfo['dir'];
2084                        $path = $fileinfo['path'];
2085                        $filename = $fileinfo['name'];
2086                        $legal_newurl = $fileinfo['legal_newurl'];
2087                        $newdir = $fileinfo['newdir'];
2088                        $newpath = $fileinfo['newpath'];
2089                        $newname = $fileinfo['newname'];
2090                        $rename = $fileinfo['rename'];
2091                        $is_dir = $fileinfo['is_dir'];
2092                        $fn = $fileinfo['function'];
2093
2094                        if($rename)
2095                        {
2096                                // try to remove the thumbnail related to the original file; don't mind if it doesn't exist
2097                                if(!$is_dir)
2098                                {
2099                                        if (!$this->deleteThumb($legal_url . $filename))
2100                                                throw new FileManagerException('delete_thumbnail_failed');
2101                                }
2102                        }
2103
2104                        if (!@$fn($path, $newpath))
2105                                throw new FileManagerException($fn . '_failed:' . $legal_newurl . ':' . $newname);
2106
2107                        if (!headers_sent()) header('Content-Type: application/json');
2108
2109                        echo json_encode(array(
2110                                'status' => 1,
2111                                'name' => $newname
2112                        ));
2113                        return;
2114                }
2115                catch(FileManagerException $e)
2116                {
2117                        $emsg = $e->getMessage();
2118                }
2119                catch(Exception $e)
2120                {
2121                        // catching other severe failures; since this can be anything and should only happen in the direst of circumstances, we don't bother translating
2122                        $emsg = $e->getMessage();
2123                }
2124
2125                $this->modify_json4exception($jserr, $emsg);
2126
2127                if (!headers_sent()) header('Content-Type: application/json');
2128
2129                // when we fail here, it's pretty darn bad and nothing to it.
2130                // just push the error JSON as go.
2131                echo json_encode($jserr);
2132        }
2133
2134
2135
2136
2137
2138
2139
2140        /**
2141         * Convert a given file spec into a URL pointing at our JiT thumbnail creation/delivery event handler.
2142         *
2143         * The spec must be an array with these elements:
2144         *   'event':       'thumbnail'
2145         *   'directory':   URI path to directory of the ORIGINAL file
2146         *   'file':        filename of the ORIGINAL file
2147         *   'size':        requested thumbnail size (e.g. 48)
2148         *   'filter':      optional mime_filter as originally specified by the client
2149         *   'type':        'thumb' or 'list': the current type of directory view at the client
2150         *
2151         * Return the URL string.
2152         */
2153        public function mkEventHandlerURL($spec)
2154        {
2155                // first determine how the client can reach us; assume that's the same URI as he went to right now.
2156                $our_handler_url = $_SERVER['SCRIPT_NAME'];
2157
2158    // Disabled the addition of propagateData here, this is done only in the client
2159                if (0 &&  is_array($this->options['URIpropagateData']))
2160                {
2161                        // the items in 'spec' always win over any entries in 'URIpropagateData':
2162                        $spec = array_merge(array(), $this->options['URIpropagateData'], $spec);
2163                }
2164
2165                // next, construct the query part of the URI:
2166                $qstr = http_build_query_ex($spec, null, '&', null, PHP_QUERY_RFC3986);
2167
2168                return $our_handler_url . '?' . $qstr;
2169        }
2170
2171
2172
2173        /**
2174         * Produce a HTML snippet detailing the given file in the JSON 'content' element; place additional info
2175         * in the JSON elements 'thumbnail', 'thumbnail48', 'thumbnail250', 'width', 'height', ...
2176         *
2177         * Return an augmented JSON array.
2178         *
2179         * Throw an exception on error.
2180         */
2181        public function extractDetailInfo($json, $legal_url, $file, $mime, $mime_filter)
2182        {
2183                $url = $this->legal2abs_url_path($legal_url);
2184                $filename = pathinfo($url, PATHINFO_BASENAME);
2185
2186                $isdir = !is_file($file);
2187                if (!$isdir)
2188                {
2189                        $iconspec = $filename;
2190                }
2191                else if (is_dir($file))
2192                {
2193                        $mime = 'text/directory';
2194                        $iconspec = ($filename == '..' ? 'is.dir_up' : 'is.dir');
2195                }
2196                else
2197                {
2198                        // simply do NOT list anything that we cannot cope with.
2199                        // That includes clearly inaccessible files (and paths) with non-ASCII characters:
2200                        // PHP5 and below are a real mess when it comes to handling Unicode filesystems
2201                        // (see the php.net site too: readdir / glob / etc. user comments and the official
2202                        // notice that PHP will support filesystem UTF-8/Unicode only when PHP6 is released.
2203                        //
2204                        // Big, fat bummer!
2205                        throw new FileManagerException('nofile');
2206                }
2207
2208                if (FileManagerUtility::startsWith($mime, 'image/'))
2209                {
2210                        /*
2211                         * offload the thumbnailing process to another event ('event=thumbnail') to be fired by the client
2212                         * when it's time to render the thumbnail: the offloading helps us tremendously in coping with large
2213                         * directories:
2214                         * WE simply assume the thumbnail will be there, so we don't even need to check for its existence
2215                         * (which saves us one more file_exists() per item at the very least). And when it doesn't, that's
2216                         * for the event=thumbnail handler to worry about (creating the thumbnail on demand or serving
2217                         * a generic icon image instead).
2218                         */
2219                        $thumb48 = $this->mkEventHandlerURL(array(
2220                                        'event' => 'thumbnail',
2221                                        // directory and filename of the ORIGINAL image should follow next:
2222                                        'directory' => pathinfo($legal_url, PATHINFO_DIRNAME),
2223                                        'file' => pathinfo($legal_url, PATHINFO_BASENAME),
2224                                        'size' => 48,          // thumbnail suitable for 'view/type=thumb' list views
2225                                        'filter' => $mime_filter
2226                                ));
2227                        $thumb250 = $this->onThumbnail(array(
2228                                        'event' => 'thumbnail',
2229                                        // directory and filename of the ORIGINAL image should follow next:
2230                                        'directory' => pathinfo($legal_url, PATHINFO_DIRNAME),
2231                                        'file' => pathinfo($legal_url, PATHINFO_BASENAME),
2232                                        'size' => 250,         // thumbnail suitable for 'view/type=thumb' list views
2233                                        'filter' => $mime_filter
2234                                ));
2235                }
2236                else
2237                {
2238                        $thumb48 = $this->getIcon($iconspec, false);
2239                        $thumb250 = $thumb48;
2240                }
2241                $icon = $this->getIcon($iconspec, true);
2242
2243                $json = array_merge(array(
2244                                //'status' => 1,
2245                                //'mimetype' => $mime,
2246                                'content' => self::compressHTML('<div class="margin">
2247                                        ${nopreview}
2248                                </div>')
2249                        ),
2250                        (is_array($json) ? $json : array()),
2251                        array(
2252                                'path' => FileManagerUtility::rawurlencode_path($url),
2253                                'name' => preg_replace('/[^ -~]/', '?', $filename),       // HACK/TWEAK: PHP5 and below are completely b0rked when it comes to international filenames   :-(
2254                                'date' => date($this->options['dateFormat'], @filemtime($file)),
2255                                'mime' => $mime,
2256                                //'thumbnail' => $thumb,
2257                                'thumbnail48' => $thumb48,
2258                                'thumbnail250' => $thumb250,
2259                                'icon' => FileManagerUtility::rawurlencode_path($icon),
2260                                'size' => @filesize($file)
2261                        ));
2262
2263
2264                // getID3 is slower as it *copies* the image to the temp dir before processing: see GetDataImageSize().
2265                // This is done as getID3 can also analyze *embedded* images, for which this approach is required.
2266                $getid3 = new getID3();
2267                $getid3->encoding = 'UTF-8';
2268                $getid3->Analyze($file);
2269
2270                $content = null;
2271
2272                if (FileManagerUtility::startsWith($mime, 'image/'))
2273                {
2274                        // generates a random number to put on the end of the image, to prevent caching
2275                        //$randomImage = '?'.md5(uniqid(rand(),1));
2276
2277                        //$size = @getimagesize($file);
2278                        //// check for badly formatted image files (corruption); we'll handle the overly large ones next
2279                        //if (!$size)
2280                        //  throw new FileManagerException('corrupt_img:' . $url);
2281
2282                        $sw_make = $this->getID3infoItem($getid3, null, 'jpg', 'exif', 'IFD0', 'Software');
2283                        $time_make = $this->getID3infoItem($getid3, null, 'jpg', 'exif', 'IFD0', 'DateTime');
2284
2285                        $width = $this->getID3infoItem($getid3, 0, 'video', 'resolution_x');
2286                        $height = $this->getID3infoItem($getid3, 0, 'video', 'resolution_y');
2287                        $json['width'] = $width;
2288                        $json['height'] = $height;
2289
2290                        $content = '<dl>
2291                                        <dt>${width}</dt><dd>' . $width . 'px</dd>
2292                                        <dt>${height}</dt><dd>' . $height . 'px</dd>
2293                                </dl>';
2294                        if (!empty($sw_make) || !empty($time_make))
2295                        {
2296                                $content .= '<p>Made with ' . (empty($sw_make) ? '???' : $sw_make) . ' @ ' . (empty($time_make) ? '???' : $time_make) . '</p>';
2297                        }
2298                        $content .= '
2299                                <h2>${preview}</h2>
2300                                ';
2301
2302                        $emsg = null;
2303                        try
2304                        {
2305        $thumbfile = $thumb250;
2306                               
2307                                // sleemanj (gogo @ Xinha): the below makes not a lot of sense I can think of,
2308                                //    if you are creating the thumbnail anyway then what is the logic
2309                                //    in requiring a round trip through the backend to display it?
2310                                //    Anyway, doesn't matter now because we created it "through" the backend
2311                                //    as thumb250 above.
2312                               
2313                                // $thumbfile = $this->getThumb($legal_url, $file, 250, 250);                           
2314                                /*
2315                                 * the thumbnail may be produced now, but we want to stay in control when the thumbnail is
2316                                 * fetched by the client, so we force them to travel through this backend.
2317                                 */
2318                                $enc_thumbfile = $thumb250;
2319                        }
2320                        catch (Exception $e)
2321                        {
2322                                $emsg = $e->getMessage();
2323                                $thumbfile = $this->getIconForError($emsg, $legal_url, false);
2324                                $enc_thumbfile = FileManagerUtility::rawurlencode_path($thumbfile);
2325
2326                                $json['thumbnail48'] = $thumbfile;
2327                                $json['thumbnail250'] = $thumbfile;
2328                        }
2329
2330                        // get the size of the thumbnail/icon: the <img> is styled with width and height to ensure the background 'loader' image is shown correctly:
2331                        $tnpath = $this->url_path2file_path($thumbfile);
2332                        $tninf = @getimagesize($tnpath);
2333
2334                        $json['tn_width'] = $tninf[0];
2335                        $json['tn_height'] = $tninf[1];
2336
2337                        $content .= '<a href="' . FileManagerUtility::rawurlencode_path($url) . '" data-milkbox="single" title="' . htmlentities($filename, ENT_QUOTES, 'UTF-8') . '">
2338                                                   <img src="' . $enc_thumbfile . '" class="preview" alt="preview" style="width: ' . $tninf[0] . 'px; height: ' . $tninf[1] . 'px;" />
2339                                                 </a>';
2340                        if (!empty($emsg))
2341                        {
2342                                // use the abilities of modify_json4exception() to munge/format the exception message:
2343                                $jsa = array();
2344                                $this->modify_json4exception($jsa, $emsg);
2345                                $content .= "\n" . '<p class="err_info">' . $jsa['error'] . '</p>';
2346                        }
2347                        if (!empty($emsg) && strpos($emsg, 'img_will_not_fit') !== false)
2348                        {
2349                                $earr = explode(':', $e->getMessage(), 2);
2350                                $content .= "\n" . '<p class="tech_info">Estimated minimum memory requirements to create thumbnails for this image: ' . $earr[1] . '</p>';
2351                        }
2352
2353                        if (DEVELOPMENT)
2354                        {
2355                                $finfo = Image::guestimateRequiredMemorySpace($file);
2356                                if (!empty($finfo['usage_guestimate']) && !empty($finfo['usage_min_advised']))
2357                                {
2358                                        $content .= "\n" . '<p class="tech_info">memory used: ' . number_format(memory_get_peak_usage() / 1E6, 1) . ' MB / estimated: ' . number_format($finfo['usage_guestimate'] / 1E6, 1) . ' MB / suggested: ' . number_format($finfo['usage_min_advised'] / 1E6, 1) . ' MB</p>';
2359                                }
2360                        }
2361
2362                        $exif_data = $this->getID3infoItem($getid3, null, 'jpg', 'exif');
2363                        try
2364                        {
2365                                if (!empty($exif_data))
2366                                {
2367                                        /*
2368                                         * before dumping the EXIF data array (which may carry binary content and MAY CRASH the json_encode()r >:-((
2369                                         * we filter it to prevent such crashes and oddly looking (diagnostic) presentation of values.
2370                                         */
2371                                        self::clean_EXIF_results($exif_data);
2372                                        ob_start();
2373                                                var_dump($exif_data);
2374                                        $dump = ob_get_clean();
2375                                        $content .= $dump;
2376                                }
2377                        }
2378                        catch (Exception $e)
2379                        {
2380                                // use the abilities of modify_json4exception() to munge/format the exception message:
2381                                $jsa = array('error' => '');
2382                                $this->modify_json4exception($jsa, $e->getMessage());
2383                                $content .= "\n" . '<p class="err_info">' . $jsa['error'] . '</p>';
2384                        }
2385                }
2386                elseif (FileManagerUtility::startsWith($mime, 'text/') || $mime == 'application/x-javascript')
2387                {
2388                        // text preview:
2389                        $filecontent = @file_get_contents($file, false, null, 0);
2390                        if ($filecontent === false)
2391                                throw new FileManagerException('nofile');
2392
2393                        if (!FileManagerUtility::isBinary($filecontent))
2394                        {
2395                                $content = '<div class="textpreview"><pre>' . str_replace(array('$', "\t"), array('&#36;', '&nbsp;&nbsp;'), htmlentities($filecontent, ENT_NOQUOTES, 'UTF-8')) . '</pre></div>';
2396                        }
2397                        // else: fall back to 'no preview available'
2398                }
2399                elseif ($mime == 'application/zip')
2400                {
2401                        $out = array(array(), array());
2402                        $info = $this->getID3infoItem($getid3, null, 'zip', 'files');
2403                        if (is_array($info))
2404                        {
2405                                foreach ($info as $name => $size)
2406                                {
2407                                        $isdir = is_array($size) ? true : false;
2408                                        $out[($isdir) ? 0 : 1][$name] = '<li><a><img src="' . FileManagerUtility::rawurlencode_path($this->getIcon($name, true)) . '" alt="" /> ' . $name . '</a></li>';
2409                                }
2410                                natcasesort($out[0]);
2411                                natcasesort($out[1]);
2412                                $content = '<ul>' . implode(array_merge($out[0], $out[1])) . '</ul>';
2413                        }
2414                }
2415                elseif ($mime == 'application/x-shockwave-flash')
2416                {
2417                        $info = $this->getID3infoItem($getid3, null, 'swf', 'header');
2418                        if (is_array($info))
2419                        {
2420                                // Note: preview data= urls were formatted like this in CCMS:
2421                                // $this->options['assetBasePath'] . 'dewplayer.swf?mp3=' . rawurlencode($url) . '&volume=30'
2422
2423                                $width = $this->getID3infoItem($getid3, 0, 'swf', 'header', 'frame_width') / 10;
2424                                $height = $this->getID3infoItem($getid3, 0, 'swf', 'header', 'frame_height') / 10;
2425                                $json['width'] = $width;
2426                                $json['height'] = $height;
2427
2428                                $content = '<dl>
2429                                                <dt>${width}</dt><dd>' . $width . 'px</dd>
2430                                                <dt>${height}</dt><dd>' . $height . 'px</dd>
2431                                                <dt>${length}</dt><dd>' . round($this->getID3infoItem($getid3, 0, 'swf', 'header', 'length') / $this->getID3infoItem($getid3, 25, 'swf', 'header', 'frame_count')) . 's</dd>
2432                                        </dl>
2433                                        <h2>${preview}</h2>
2434                                        <div class="object">
2435                                                <object type="application/x-shockwave-flash" data="'.FileManagerUtility::rawurlencode_path($url).'" width="500" height="400">
2436                                                        <param name="scale" value="noscale" />
2437                                                        <param name="movie" value="'.FileManagerUtility::rawurlencode_path($url).'" />
2438                                                </object>
2439                                        </div>';
2440                        }
2441                }
2442                elseif (FileManagerUtility::startsWith($mime, 'audio/'))
2443                {
2444                        getid3_lib::CopyTagsToComments($getid3->info);
2445
2446                        $dewplayer = FileManagerUtility::rawurlencode_path($this->options['assetBasePath'] . 'dewplayer.swf');
2447
2448                        $content = '<dl>
2449                                        <dt>${title}</dt><dd>' . $this->getID3infoItem($getid3, '???', 'comments', 'title', 0) . '</dd>
2450                                        <dt>${artist}</dt><dd>' . $this->getID3infoItem($getid3, '???', 'comments', 'artist', 0) . '</dd>
2451                                        <dt>${album}</dt><dd>' . $this->getID3infoItem($getid3, '???', 'comments', 'album', 0) . '</dd>
2452                                        <dt>${length}</dt><dd>' . $this->getID3infoItem($getid3, '???', 'playtime_string') . '</dd>
2453                                        <dt>${bitrate}</dt><dd>' . round($this->getID3infoItem($getid3, 0, 'bitrate') / 1000) . 'kbps</dd>
2454                                </dl>
2455                                <h2>${preview}</h2>
2456                                <div class="object">
2457                                        <object type="application/x-shockwave-flash" data="' . $dewplayer . '" width="200" height="20" id="dewplayer" name="dewplayer">
2458                                                <param name="wmode" value="transparent" />
2459                                                <param name="movie" value="' . $dewplayer . '" />
2460                                                <param name="flashvars" value="mp3=' . FileManagerUtility::rawurlencode_path($url) . '&amp;volume=50&amp;showtime=1" />
2461                                        </object>
2462                                </div>';
2463                }
2464                elseif (FileManagerUtility::startsWith($mime, 'video/'))
2465                {
2466                        $dewplayer = FileManagerUtility::rawurlencode_path($this->options['assetBasePath'] . 'dewplayer.swf');
2467
2468                        $a_fmt = $this->getID3infoItem($getid3, '???', 'audio', 'dataformat');
2469                        $a_samplerate = $this->getID3infoItem($getid3, 0, 'audio', 'sample_rate') / 1000;
2470                        $a_bitrate = round($this->getID3infoItem($getid3, 0, 'audio', 'bitrate') / 1000);
2471                        $a_bitrate_mode = $this->getID3infoItem($getid3, '???', 'audio', 'bitrate_mode');
2472                        $a_channels = $this->getID3infoItem($getid3, 0, 'audio', 'channels');
2473                        $a_codec = $this->getID3infoItem($getid3, '', 'audio', 'codec');
2474                        $a_streams = $this->getID3infoItem($getid3, '???', 'audio', 'streams');
2475                        $a_streamcount = (is_array($a_streams) ? count($a_streams) : 0);
2476
2477                        $v_fmt = $this->getID3infoItem($getid3, '???', 'video', 'dataformat');
2478                        $v_bitrate = round($this->getID3infoItem($getid3, 0, 'video', 'bitrate') / 1000);
2479                        $v_bitrate_mode = $this->getID3infoItem($getid3, '???', 'video', 'bitrate_mode');
2480                        $v_framerate = $this->getID3infoItem($getid3, '???', 'video', 'frame_rate');
2481                        $v_width = $this->getID3infoItem($getid3, '???', 'video', 'resolution_x');
2482                        $v_height = $this->getID3infoItem($getid3, '???', 'video', 'resolution_y');
2483                        $v_par = $this->getID3infoItem($getid3, 1.0, 'video', 'pixel_aspect_ratio');
2484                        $v_codec = $this->getID3infoItem($getid3, '', 'video', 'codec');
2485
2486                        $g_bitrate = round($this->getID3infoItem($getid3, 0, 'bitrate') / 1000);
2487                        $g_playtime_str = $this->getID3infoItem($getid3, '???', 'playtime_string');
2488
2489                        $content = '<dl>
2490                                        <dt>Audio</dt><dd>' . $a_fmt . (!empty($a_codec) ? ' (' . $a_codec . ')' : '') .
2491                                                                                (!empty($a_channels) ? ($a_channels === 1 ? ' (mono)' : $a_channels === 2 ? ' (stereo)' : ' (' . $a_channels . ' channels)') : '') .
2492                                                                                ': ' . $a_samplerate . ' kHz @ ' . $a_bitrate . ' kbps (' . strtoupper($a_bitrate_mode) . ')' .
2493                                                                                ($a_streamcount > 1 ? ' (' . $a_streamcount . ' streams)' : '') .
2494                                                                '</dd>
2495                                        <dt>Video</dt><dd>' . $v_fmt . (!empty($v_codec) ? ' (' . $v_codec . ')' : '') .  ': ' . $v_framerate . ' fps @ ' . $v_bitrate . ' kbps (' . strtoupper($v_bitrate_mode) . ')' .
2496                                                                                ($v_par != 1.0 ? ', PAR: ' . $v_par : '') .
2497                                                                '</dd>
2498                                        <dt>${width}</dt><dd>' . $v_width . 'px</dd>
2499                                        <dt>${height}</dt><dd>' . $v_height . 'px</dd>
2500                                        <dt>${length}</dt><dd>' . $g_playtime_str . '</dd>
2501                                        <dt>${bitrate}</dt><dd>' . $g_bitrate . 'kbps</dd>
2502                                </dl>
2503                                <h2>${preview}</h2>
2504                                <div class="object">
2505                                        <object type="application/x-shockwave-flash" data="' . $dewplayer . '" width="200" height="20" id="dewplayer" name="dewplayer">
2506                                                <param name="wmode" value="transparent" />
2507                                                <param name="movie" value="' . $dewplayer . '" />
2508                                                <param name="flashvars" value="mp3=' . FileManagerUtility::rawurlencode_path($url) . '&amp;volume=50&amp;showtime=1" />
2509                                        </object>
2510                                </div>';
2511                }
2512                else
2513                {
2514                        // else: fall back to 'no preview available'
2515                        if (!empty($getid3->info) && empty($getid3->info['error']))
2516                        {
2517                                try
2518                                {
2519                                        ob_start();
2520                                                var_dump($getid3->info);
2521                                        $dump = ob_get_clean();
2522                                        // $dump may dump object IDs and other binary stuff, which will completely b0rk json_encode: make it palatable:
2523
2524                                        // strip the NULs out:
2525                                        $dump = str_replace('&#0;', '?', $dump);
2526                                        //$dump = html_entity_decode(strip_tags($dump), ENT_QUOTES, 'UTF-8');
2527                                        //@file_put_contents('getid3.raw.log', $dump);
2528                                        // since the regex matcher leaves NUL bytes alone, we do those above in undecoded form; the rest is treated here
2529                                        $dump = preg_replace("/[^ -~\n\r\t]/", '?', $dump); // remove everything outside ASCII range; some of the high byte values seem to crash json_encode()!
2530                                        // and reduce long sequences of unknown charcodes:
2531                                        $dump = preg_replace('/\?{8,}/', '???????', $dump);
2532                                        //$dump = html_entity_encode(strip_tags($dump), ENT_NOQUOTES, 'UTF-8');
2533
2534                                        $content = '<div class="margin">
2535                                                                <h2>${preview}</h2>
2536                                                                <pre>' . "\n" . $dump . "\n" . '</pre></div>';
2537                                        //@file_put_contents('getid3.log', $dump);
2538                                }
2539                                catch(Exception $e)
2540                                {
2541                                        // ignore
2542                                        $content = '<div class="margin">
2543                                                                ${nopreview}
2544                                                                <p class="err_info">' . $e->getMessage() . '</p>
2545                                                        </div>';
2546                                }
2547                        }
2548                        else
2549                        {
2550                                $content = '<div class="margin">
2551                                                        ${nopreview}
2552                                                        <p class="err_info">' . implode(', ', $getid3->info['error']) . '</p>
2553                                                </div>';
2554                        }
2555                }
2556
2557                if (!empty($content))
2558                {
2559                        $json['content'] = self::compressHTML($content);
2560                }
2561
2562                return $json;
2563        }
2564
2565        /**
2566         * Traverse the getID3 info[] array tree and fetch the item pointed at by the variable number of indices specified
2567         * as additional parameters to this function.
2568         *
2569         * Return the default value when the indicated element does not exist in the info[] set; otherwise return the located item.
2570         *
2571         * The purpose of this method is to act as a safe go-in-between for the fileManager to collect arbitrary getID3 data and
2572         * not get a PHP error when some item in there does not exist.
2573         */
2574        public /* static */ function getID3infoItem($getid3_obj, $default_value /* , ... */ )
2575        {
2576                $rv = false;
2577                $o = $getid3_obj->info;
2578                $argc = func_num_args();
2579
2580                for ($i = 2; $i < $argc; $i++)
2581                {
2582                        if (!is_array($o))
2583                        {
2584                                return $default_value;
2585                        }
2586
2587                        $index = func_get_arg($i);
2588                        if (array_key_exists($index, $o))
2589                        {
2590                                $o = $o[$index];
2591                        }
2592                        else
2593                        {
2594                                return $default_value;
2595                        }
2596                }
2597                return $o;
2598        }
2599
2600        // helper function for clean_EXIF_results() as PHP < 5.3 lacks lambda functions
2601        protected static function __clean_EXIF_results(&$value, $key)
2602        {
2603                if (is_string($value))
2604                {
2605                        if (FileManagerUtility::isBinary($value))
2606                        {
2607                                $value = '(binary data... length = ' . strlen($value) . ')';
2608                        }
2609                }
2610        }
2611
2612        protected static function clean_EXIF_results(&$arr)
2613        {
2614                // see http://nl2.php.net/manual/en/function.array-walk-recursive.php#81835
2615                // --> we don't mind about it because we're not worried about the references occurring in here, now or later.
2616                // Indeed, that does assume we (as in 'we' being this particular function!) know about how the
2617                // data we process will be used. Risky, but fine with me. Hence the 'protected'.
2618                array_walk_recursive($arr, 'self::__clean_EXIF_results');
2619        }
2620
2621        /**
2622         * Delete a file or directory, inclusing subdirectories and files.
2623         *
2624         * Return TRUE on success, FALSE when an error occurred.
2625         *
2626         * Note that the routine will try to percevere and keep deleting other subdirectories
2627         * and files, even when an error occurred for one or more of the subitems: this is
2628         * a best effort policy.
2629         */
2630        protected function unlink($legal_url, $mime_filters)
2631        {
2632                $rv = true;
2633
2634                // must transform here so alias/etc. expansions inside legal_url_path2file_path() get a chance:
2635                $file = $this->legal_url_path2file_path($legal_url);
2636
2637                if(is_dir($file))
2638                {
2639                        $dir = self::enforceTrailingSlash($file);
2640                        $url = self::enforceTrailingSlash($legal_url);
2641                        $files = $this->scandir($dir);
2642                        foreach ($files as $f)
2643                        {
2644                                if(in_array($f, array('.','..')))
2645                                        continue;
2646
2647                                $rv2 = $this->unlink($url . $f, $mime_filters);
2648                                if ($rv2)
2649                                        $rv &= $this->deleteThumb($url . $f);
2650                                else
2651                                        $rv = false;
2652                        }
2653
2654                        $rv &= @rmdir($file);
2655                }
2656                else if (file_exists($file))
2657                {
2658                        if (is_file($file))
2659                        {
2660                                $mime = $this->getMimeType($file);
2661                                if (!$this->IsAllowedMimeType($mime, $mime_filters))
2662                                        return false;
2663                        }
2664
2665                        $rv2 = @unlink($file);
2666                        if ($rv2)
2667                                $rv &= $this->deleteThumb($legal_url);
2668                        else
2669                                $rv = false;
2670                }
2671                return $rv;
2672        }
2673
2674        /**
2675         * glob() wrapper: accepts the same options as Tooling.php::safe_glob()
2676         *
2677         * However, this method will also ensure the '..' directory entry is only returned,
2678         * even while asked for, when the parent directory can be legally traversed by the FileManager.
2679         *
2680         * Always return an array (possibly empty)
2681         *
2682         * IMPORTANT: this function GUARANTEES that, when present at all, the double-dot '..'
2683         *            entry is the very last entry in the array.
2684         *            This guarantee is important as onView() et al depend on it.
2685         */
2686        protected function scandir($dir, $filemask = '*', $see_thumbnail_dir = false)
2687        {
2688                // list files, except the thumbnail folder itself or any file in it:
2689                $dir = self::enforceTrailingSlash($dir);
2690
2691                $just_below_thumbnail_dir = false;
2692                if (!$see_thumbnail_dir)
2693                {
2694                        $tnpath = $this->url_path2file_path($this->options['thumbnailPath']);
2695                        if (FileManagerUtility::startswith($dir, $tnpath))
2696                                return false;
2697
2698                        $tnparent = $this->url_path2file_path(self::getParentDir($this->options['thumbnailPath']));
2699                        $just_below_thumbnail_dir = ($dir == $tnparent);
2700
2701                        $tndir = basename(substr($this->options['thumbnailPath'], 0, -1));
2702                }
2703
2704                $at_basedir = ($this->url_path2file_path($this->options['directory']) == $dir);
2705
2706
2707                $files = safe_glob($dir . $filemask, GLOB_NODOTS | GLOB_NOSORT);
2708
2709
2710                if ($just_below_thumbnail_dir)
2711                {
2712                        $f = array();
2713                        foreach($files as $file)
2714                        {
2715                                if ($file !== $tndir)
2716                                        $f[] = $file;
2717                        }
2718                        unset($files);
2719                        $files = $f;
2720                }
2721
2722                if (!$at_basedir)
2723                {
2724                        $files[] = '..';
2725                }
2726
2727                return $files;
2728        }
2729
2730        /**
2731         * Make a cleaned-up, unique filename
2732         *
2733         * Return the file (dir + name + ext), or a unique, yet non-existing, variant thereof, where the filename
2734         * is appended with a '_' and a number, e.g. '_1', when the file itself already exists in the given
2735         * directory. The directory part of the returned value equals $dir.
2736         *
2737         * Return NULL when $file is empty or when the specified directory does not reside within the
2738         * directory tree rooted by options['directory']
2739         *
2740         * Note that the given filename will be converted to a legal filename, containing a filesystem-legal
2741         * subset of ASCII characters only, before being used and returned by this function.
2742         *
2743         * @param mixed $fileinfo     either a string containing a filename+ext or an array as produced by pathinfo().
2744         * @daram string $dir         path pointing at where the given file may exist.
2745         *
2746         * @return a filepath consisting of $dir and the cleaned up and possibly sequenced filename and file extension
2747         *         as provided by $fileinfo.
2748         */
2749        protected function getUniqueName($fileinfo, $dir)
2750        {
2751                $dir = self::enforceTrailingSlash($dir);
2752
2753                if (is_string($fileinfo))
2754                {
2755                        $fileinfo = pathinfo($fileinfo);
2756                }
2757
2758                if (!is_array($fileinfo) || !$fileinfo['filename']) return null;
2759
2760
2761                /*
2762                 * since 'pagetitle()' is used to produce a unique, non-existing filename, we can forego the dirscan
2763                 * and simply check whether the constructed filename/path exists or not and bump the suffix number
2764                 * by 1 until it does not, thus quickly producing a unique filename.
2765                 *
2766                 * This is faster than using a dirscan to collect a set of existing filenames and feeding them as
2767                 * an option array to pagetitle(), particularly for large directories.
2768                 */
2769                $filename = FileManagerUtility::pagetitle($fileinfo['filename'], null, '-_., []()~!@+' /* . '#&' */, '-_,~@+#&');
2770                if (!$filename)
2771                        return null;
2772
2773                // also clean up the extension: only allow alphanumerics in there!
2774                $ext = FileManagerUtility::pagetitle(!empty($fileinfo['extension']) ? $fileinfo['extension'] : null);
2775                $ext = (!empty($ext) ? '.' . $ext : null);
2776                // make sure the generated filename is SAFE:
2777                $fname = $filename . $ext;
2778                $file = $dir . $fname;
2779                if (file_exists($file))
2780                {
2781                        /*
2782                         * make a unique name. Do this by postfixing the filename with '_X' where X is a sequential number.
2783                         *
2784                         * Note that when the input name is already such a 'sequenced' name, the sequence number is
2785                         * extracted from it and sequencing continues from there, hence input 'file_5' would, if it already
2786                         * existed, thus be bumped up to become 'file_6' and so on, until a filename is found which
2787                         * does not yet exist in the designated directory.
2788                         */
2789                        $i = 1;
2790                        if (preg_match('/^(.*)_([1-9][0-9]*)$/', $filename, $matches))
2791                        {
2792                                $i = intval($matches[2]);
2793                                if ('P'.$i != 'P'.$matches[2] || $i > 100000)
2794                                {
2795                                        // very large number: not a sequence number!
2796                                        $i = 1;
2797                                }
2798                                else
2799                                {
2800                                        $filename = $matches[1];
2801                                }
2802                        }
2803                        do
2804                        {
2805                                $fname = $filename . ($i ? '_' . $i : '') . $ext;
2806                                $file = $dir . $fname;
2807                                $i++;
2808                        } while (file_exists($file));
2809                }
2810
2811                // $fname is now guaranteed to NOT exist in the given directory
2812                return $fname;
2813        }
2814
2815        /**
2816         * Returns the URI path to the apropriate icon image for the given file / directory.
2817         *
2818         * NOTES:
2819         *
2820         * 1) any $path with an 'extension' of '.dir' is assumed to be a directory.
2821         *
2822         * 2) This method specifically does NOT check whether the given path exists or not: it just looks at
2823         *    the filename extension passed to it, that's all.
2824         *
2825         * Note #2 is important as this enables this function to also serve as icon fetcher for ZIP content viewer, etc.:
2826         * after all, those files do not exist physically on disk themselves!
2827         */
2828        public function getIcon($file, $smallIcon)
2829        {
2830                $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
2831
2832                $largeDir = (!$smallIcon ? 'Large/' : '');
2833                $url_path = $this->options['assetBasePath'] . 'Images/Icons/' .$largeDir.$ext.'.png';
2834                $path = (is_file($this->url_path2file_path($url_path)))
2835                        ? $url_path
2836                        : $this->options['assetBasePath'] . 'Images/Icons/'.$largeDir.'default.png';
2837
2838                return $path;
2839        }
2840
2841        /**
2842         * Return the path to the thumbnail of the specified image, the thumbnail having its
2843         * width and height limited to $width pixels.
2844         *
2845         * When the thumbnail image does not exist yet, it will created on the fly.
2846         *
2847         * @param string $legal_url    the LEGAL URL path to the original image. Is used solely
2848         *                             to generate a suitable thumbnail filename.
2849         *
2850         * @param string $path         filesystem path to the original image. Is used to derive
2851         *                             the thumbnail content from.
2852         *
2853         * @param integer $width       the maximum number of pixels for width of the
2854         *                             thumbnail.
2855         *
2856         * @param integer $height      the maximum number of pixels for height of the
2857         *                             thumbnail.
2858         */
2859        public function getThumb($legal_url, $path, $width, $height, $onlyIfExists = FALSE)
2860        {
2861                $thumb = $this->generateThumbName($legal_url, $width);
2862                $thumbPath = $this->url_path2file_path($this->options['thumbnailPath'] . $thumb);
2863                if (!is_file($thumbPath))
2864                {
2865      if($onlyIfExists) return FALSE;
2866     
2867                        if (!file_exists(dirname($thumbPath)))
2868                        {       
2869                                @mkdir(dirname($thumbPath), $this->options['chmod'], true);
2870                        }
2871                        $img = new Image($path);
2872                        // generally save as lossy / lower-Q jpeg to reduce filesize, unless orig is PNG/GIF, higher quality for smaller thumbnails:
2873                        $img->resize($width,$height)->save($thumbPath, min(98, max(MTFM_THUMBNAIL_JPEG_QUALITY, MTFM_THUMBNAIL_JPEG_QUALITY + 0.15 * (250 - min($width, $height)))), true);
2874                        unset($img);
2875                }
2876                return $this->options['thumbnailPath'] . $thumb;
2877        }
2878
2879        /**
2880         * Assitant function which produces the best possible icon image path for the given error/exception message.
2881         */
2882        public function getIconForError($emsg, $original_filename, $small_icon)
2883        {
2884                if (empty($emsg))
2885                {
2886                        // just go and pick the extension-related icon for this one; nothing is wrong today, it seems.
2887                        $thumb_path = (!empty($original_filename) ? $original_filename : 'is.default-missing');
2888                }
2889                else
2890                {
2891                        $thumb_path = 'is.default-error';
2892
2893                        if (strpos($emsg, 'img_will_not_fit') !== false)
2894                        {
2895                                $thumb_path = 'is.oversized_img';
2896                        }
2897                        else if (strpos($emsg, 'nofile') !== false)
2898                        {
2899                                $thumb_path = 'is.default-missing';
2900                        }
2901                        else if (strpos($emsg, 'unsupported_imgfmt') !== false)
2902                        {
2903                                // just go and pick the extension-related icon for this one; nothing seriously wrong here.
2904                                $thumb_path = (!empty($original_filename) ? $original_filename : $thumb_path);
2905                        }
2906                        else if (strpos($emsg, 'image') !== false)
2907                        {
2908                                $thumb_path = 'badly.broken_img';
2909                        }
2910                }
2911
2912                $img_filepath = $this->getIcon($thumb_path, $small_icon);
2913
2914                return $img_filepath;
2915        }
2916
2917        /**
2918         * Make sure the generated thumbpath is unique for each file. To prevent
2919         * reduced performance for large file sets: all thumbnails derived from any files in the entire
2920         * FileManager-managed directory tree, rooted by options['directory'], can become a huge collection,
2921         * so we distribute them across a directory tree, which is created on demand.
2922         *
2923         * The thumbnails directory tree is determined by the MD5 of the full path to the image,
2924         * using the first two characters of the MD5, making for a span of 256.
2925         *
2926         * Note: when you expect to manage a really HUGE file collection from FM, you may dial up the
2927         *       $number_of_dir_levels to 2 here.
2928         */
2929        protected function generateThumbName($legal_url, $width = 250, $number_of_dir_levels = MTFM_NUMBER_OF_DIRLEVELS_FOR_CACHE)
2930        {
2931                $fi = pathinfo($legal_url);
2932                $ext = strtolower(!empty($fi['extension']) ? $fi['extension'] : '');
2933                switch ($ext)
2934                {
2935                case 'gif':
2936                case 'png':
2937                case 'jpg':
2938                case 'jpeg':
2939                        break;
2940
2941                default:
2942                        // default to PNG, as it'll handle transparancy and full color both:
2943                        $ext = 'png';
2944                        break;
2945                }
2946
2947                // as the Thumbnail is generated, but NOT guaranteed from a safe filepath (FM may be visiting unsafe
2948                // image files when they exist in a preloaded directory tree!) we do the full safe-filename transform
2949                // on the name itself.
2950                // The MD5 is taken from the untrammeled original, though:
2951                $dircode = md5($legal_url);
2952
2953                $rv = '';
2954                for ($i = 0; $i < $number_of_dir_levels; $i++)
2955                {
2956                        $rv .= substr($dircode, 0, 2) . '/';
2957                        $dircode = substr($dircode, 2);
2958                }
2959
2960                $fn = '_' . $fi['filename'];
2961                $fn = substr($dircode, 0, 4) . preg_replace('/[^A-Za-z0-9]+/', '_', $fn);
2962                $fn = substr($fn . $dircode, 0, 38);
2963                $ext = preg_replace('/[^A-Za-z0-9_]+/', '_', $ext);
2964
2965                $rv .= $fn . '-' . $width . '.' . $ext;
2966                return $rv;
2967        }
2968
2969        protected function deleteThumb($legal_url)
2970        {
2971                // generate a thumbnail name with embedded wildcard for the size parameter:
2972                $thumb = $this->generateThumbName($legal_url, '*');
2973                $tfi = pathinfo($thumb);
2974                $thumbnail_subdir = $tfi['dirname'];
2975                $thumbPath = $this->url_path2file_path($this->options['thumbnailPath'] . $thumbnail_subdir);
2976                $thumbPath = self::enforceTrailingSlash($thumbPath);
2977
2978                // remove thumbnails (any size) and any other related cached files (TODO: future version should cache getID3 metadata as well -- and delete it here!)
2979                $files = $this->scandir($thumbPath, $tfi['filename'] . '.*', true);
2980
2981                $rv = true;
2982                if (is_array($files))
2983                {
2984                        foreach($files as $filename)
2985                        {
2986                                if(in_array($filename, array('.','..')))
2987                                        continue;
2988
2989                                $file = $thumbPath . $filename;
2990                                if(is_file($file))
2991                                        $rv &= @unlink($file);
2992                        }
2993                }
2994
2995                // as the thumbnail subdirectory may now be entirely empty, try to remove it as well,
2996                // but do NOT yack when we don't succeed: there may be other thumbnails, etc. in there still!
2997
2998                while ($thumbnail_subdir > '/')
2999                {
3000                        // try to NOT delete the thumbnails base directory itself; we MAY not be able to recreate it later on demand!
3001                        $thumbPath = $this->url_path2file_path($this->options['thumbnailPath'] . $thumbnail_subdir);
3002                        @rmdir($thumbPath);
3003
3004                        $thumbnail_subdir = self::getParentDir($thumbnail_subdir);
3005                }
3006
3007                return $rv;   // when thumbnail does not exist, say it is succesfully removed: all that counts is it doesn't exist anymore when we're done here.
3008        }
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019        /**
3020         * Safe replacement of dirname(); does not care whether the input has a trailing slash or not.
3021         *
3022         * Return FALSE when the path is attempting to get the parent of '/'
3023         */
3024        public static function getParentDir($path)
3025        {
3026                /*
3027                 * on Windows, you get:
3028                 *
3029                 * dirname("/") = "\"
3030                 * dirname("y/") = "."
3031                 * dirname("/x") = "\"
3032                 *
3033                 * so we'd rather not use dirname()   :-(
3034                 */
3035                if (!is_string($path))
3036                        return false;
3037                $path = rtrim($path, '/');
3038                // empty directory or a path with only 1 character in it cannot be a parent+child: that would be 2 at the very least when it's '/a': parent is root '/' then:
3039                if (strlen($path) <= 1)
3040                        return false;
3041
3042                $p2 = strrpos($path, '/' /* , -1 */ );  // -1 as extra offset is not good enough? Nope. At least not for my Win32 PHP 5.3.1. Yeah, sounds like a PHP bug to me. So we rtrim() now...
3043                if ($p2 === false)
3044                {
3045                        return false; // tampering!
3046                }
3047                $prev = substr($path, 0, $p2 + 1);
3048                return $prev;
3049        }
3050
3051        /**
3052         * Return the URI absolute path to the directory pointed at by the current URI request.
3053         * For example, if the request was 'http://site.org/dir1/dir2/script', then this method will
3054         * return '/dir1/dir2/'.
3055         *
3056         * Note that the path is returned WITH a trailing slash '/'.
3057         */
3058        public /* static */ function getRequestPath()
3059        {
3060                // see also: http://php.about.com/od/learnphp/qt/_SERVER_PHP.htm
3061                $path = self::getParentDir(str_replace('\\', '/', $_SERVER['SCRIPT_NAME']));
3062                $path = self::enforceTrailingSlash($path);
3063
3064                return $path;
3065        }
3066
3067        /**
3068         * Normalize an absolute path by converting all slashes '/' and/or backslashes '\' and any mix thereof in the
3069         * specified path to UNIX/MAC/Win compatible single forward slashes '/'.
3070         *
3071         * Also roll up any ./ and ../ directory elements in there.
3072         *
3073         * Throw an exception when the operation failed to produce a legal path.
3074         */
3075        public /* static */ function normalize($path)
3076        {
3077                $path = preg_replace('/(\\\|\/)+/', '/', $path);
3078
3079                /*
3080                 * fold '../' directory parts to prevent malicious paths such as 'a/../../../../../../../../../etc/'
3081                 * from succeeding
3082                 *
3083                 * to prevent screwups in the folding code, we FIRST clean out the './' directories, to prevent
3084                 * 'a/./.././.././.././.././.././.././.././.././../etc/' from succeeding:
3085                 */
3086                $path = preg_replace('#/(\./)+#', '/', $path);
3087
3088                // 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:
3089                $lead = '';
3090                // the leading part may NOT contain any directory separators, as it's for drive letters only.
3091                // So we must check in order to prevent malice like /../../../../../../../c:/dir from making it through.
3092                if (preg_match('#^([A-Za-z]:)?/(.*)$#', $path, $matches))
3093                {
3094                        $lead = $matches[1];
3095                        $path = '/' . $matches[2];
3096                }
3097
3098                while (($pos = strpos($path, '/..')) !== false)
3099                {
3100                        $prev = substr($path, 0, $pos);
3101                        /*
3102                         * on Windows, you get:
3103                         *
3104                         * dirname("/") = "\"
3105                         * dirname("y/") = "."
3106                         * dirname("/x") = "\"
3107                         *
3108                         * so we'd rather not use dirname()   :-(
3109                         */
3110                        $p2 = strrpos($prev, '/');
3111                        if ($p2 === false)
3112                        {
3113                                throw new FileManagerException('path_tampering:' . $path);
3114                        }
3115                        $prev = substr($prev, 0, $p2);
3116                        $next = substr($path, $pos + 3);
3117                        if ($next && $next[0] != '/')
3118                        {
3119                                throw new FileManagerException('path_tampering:' . $path);
3120                        }
3121                        $path = $prev . $next;
3122                }
3123
3124                $path = $lead . $path;
3125
3126                /*
3127                 * iff there was such a '../../../etc/' attempt, we'll know because there'd be an exception thrown in the loop above.
3128                 */
3129
3130                return $path;
3131        }
3132
3133
3134        /**
3135         * Accept a URI relative or absolute path and transform it to an absolute URI path, i.e. rooted against DocumentRoot.
3136         *
3137         * Relative paths are assumed to be relative to the current request path, i.e. the getRequestPath() produced path.
3138         *
3139         * Note: as it uses normalize(), any illegal path will throw an FileManagerException
3140         *
3141         * Returns a fully normalized URI absolute path.
3142         */
3143        public function rel2abs_url_path($path)
3144        {
3145                $path = str_replace('\\', '/', $path);
3146                if (!FileManagerUtility::startsWith($path, '/'))
3147                {
3148                        $based = $this->getRequestPath();
3149                        $path = $based . $path;
3150                }
3151                return $this->normalize($path);
3152        }
3153
3154        /**
3155         * Accept an absolute URI path, i.e. rooted against DocumentRoot, and transform it to a LEGAL URI absolute path, i.e. rooted against options['directory'].
3156         *
3157         * Relative paths are assumed to be relative to the current request path, i.e. the getRequestPath() produced path.
3158         *
3159         * Note: as it uses normalize(), any illegal path will throw a FileManagerException
3160         *
3161         * Returns a fully normalized LEGAL URI path.
3162         *
3163         * Throws a FileManagerException when the given path cannot be converted to a LEGAL URL, i.e. when it resides outside the options['directory'] subtree.
3164         */
3165        public function abs2legal_url_path($path)
3166        {
3167                $root = $this->options['directory'];
3168
3169                $path = $this->rel2abs_url_path($path);
3170                //$path = $this->normalize($path);    -- taken care of by rel2abs_url_path already
3171
3172                // but we MUST make sure the path is still a LEGAL URI, i.e. sutting inside options['directory']:
3173                if (strlen($path) < strlen($root))
3174                        $path = self::enforceTrailingSlash($path);
3175
3176                if (!FileManagerUtility::startsWith($path, $root))
3177                {
3178                        throw new FileManagerException('path_tampering:' . $path);
3179                }
3180
3181                // clip the trailing '/' off the $root path before reduction:
3182                $path = str_replace(substr($root, 0, -1), '', $path);
3183               
3184                return $path;
3185        }
3186
3187        /**
3188         * Accept a URI relative or absolute LEGAL URI path and transform it to an absolute URI path, i.e. rooted against DocumentRoot.
3189         *
3190         * Relative paths are assumed to be relative to the current request path, i.e. the getRequestPath() produced path.
3191         *
3192         * Note: as it uses normalize(), any illegal path will throw a FileManagerException
3193         *
3194         * Returns a fully normalized URI absolute path.
3195         */
3196        public function legal2abs_url_path($path)
3197        {
3198                $root = $this->options['directory'];
3199
3200                $path = str_replace('\\', '/', $path);
3201                if (FileManagerUtility::startsWith($path, '/'))
3202                {
3203                        // clip the trailing '/' off the $root path as $path has a leading '/' already:
3204                        $path = substr($root, 0, -1) . $path;
3205                }
3206
3207                $path = $this->rel2abs_url_path($path);
3208                //$path = $this->normalize($path);    -- taken care of by rel2abs_url_path already
3209
3210                // but we MUST make sure the path is still a LEGAL URI, i.e. sutting inside options['directory']:
3211                if (strlen($path) < strlen($root))
3212                        $path = self::enforceTrailingSlash($path);
3213
3214                if (!FileManagerUtility::startsWith($path, $root))
3215                {
3216                        throw new FileManagerException('path_tampering:' . $path);
3217                }
3218                return $path;
3219        }
3220
3221        /**
3222         * Accept a URI relative or absolute LEGAL URI path and transform it to an absolute LEGAL URI path, i.e. rooted against options['directory'].
3223         *
3224         * Relative paths are assumed to be relative to the options['directory'] directory. This makes them equivalent to absolute paths within
3225         * the LEGAL URI tree and this fact may seem odd. Alas, the FM frontend sends requests without the leading slash and it's those that
3226         * we wish to resolve here, after all. So, yes, this deviates from the general principle applied elesewhere in the code. :-(
3227         * Nevertheless, it's easier than scanning and tweaking the FM frontend everywhere.
3228         *
3229         * Note: as it uses normalize(), any illegal path will throw an FileManagerException
3230         *
3231         * Returns a fully normalized LEGAL URI absolute path.
3232         */
3233        public function rel2abs_legal_url_path($path)
3234        {
3235                if (0) // TODO: remove the 'relative is based on options['directory']' hack when the frontend has been fixed...
3236                {
3237                        $path = $this->legal2abs_url_path($path);
3238
3239                        $root = $this->options['directory'];
3240
3241                        // clip the trailing '/' off the $root path before reduction:
3242                        $path = str_replace(substr($root, 0, -1), '', $path);
3243                }
3244                else
3245                {
3246                        $path = str_replace('\\', '/', $path);
3247                        if (!FileManagerUtility::startsWith($path, '/'))
3248                        {
3249                                $path = '/' . $path;
3250                        }
3251
3252                        $path = $this->normalize($path);
3253                }
3254
3255                return $path;
3256        }
3257
3258        /**
3259         * Return the filesystem absolute path for the relative or absolute URI path.
3260         *
3261         * Note: as it uses normalize(), any illegal path will throw an FileManagerException
3262         *
3263         * Returns a fully normalized filesystem absolute path.
3264         */
3265        public function url_path2file_path($url_path)
3266        {
3267                $url_path = $this->rel2abs_url_path($url_path);
3268
3269                $root = str_replace('\\', '/', $_SERVER['DOCUMENT_ROOT']);
3270                if (FileManagerUtility::endsWith($root, '/'))
3271                {
3272                        $root = substr($root, 0, -1);
3273                }
3274                $path = $root . $url_path;
3275                //$path = $this->normalize($path);    -- taken care of by rel2abs_url_path already
3276                return $path;
3277        }
3278
3279        /**
3280         * Return the filesystem absolute path for the relative URI path or absolute LEGAL URI path.
3281         *
3282         * Note: as it uses normalize(), any illegal path will throw an FileManagerException
3283         *
3284         * Returns a fully normalized filesystem absolute path.
3285         */
3286        public function legal_url_path2file_path($url_path)
3287        {
3288                $path = $this->rel2abs_legal_url_path($url_path);
3289
3290                $path = substr($this->options['directory'], 0, -1) . $path;
3291
3292                $path = $this->url_path2file_path($path);
3293
3294                return $path;
3295        }
3296
3297        public static function enforceTrailingSlash($string)
3298        {
3299                return (strrpos($string, '/') === strlen($string) - 1 ? $string : $string . '/');
3300        }
3301
3302
3303
3304
3305
3306        /**
3307         * Produce minimized HTML output; used to cut don't on the content fed
3308         * to JSON_encode() and make it more readable in raw debug view.
3309         */
3310        public static function compressHTML($str)
3311        {
3312                // brute force: replace tabs by spaces and reduce whitespace series to a single space.
3313                //$str = preg_replace('/\s+/', ' ', $str);
3314
3315                return $str;
3316        }
3317
3318
3319        protected /* static */ function modify_json4exception(&$jserr, $emsg, $mode = 0)
3320        {
3321                if (empty($emsg))
3322                        return;
3323
3324                // only set up the new json error report array when this is the first exception we got:
3325                if (empty($jserr['error']))
3326                {
3327                        // check the error message and see if it is a translation code word (with or without parameters) or just a generic error report string
3328                        $e = explode(':', $emsg, 2);
3329                        if (preg_match('/[^A-Za-z0-9_-]/', $e[0]))
3330                        {
3331                                // generic message. ouch.
3332                                $jserr['error'] = $emsg;
3333                        }
3334                        else
3335                        {
3336                                $jserr['error'] = $emsg = '${backend.' . $e[0] . '}' . (isset($e[1]) ? $e[1] : '');
3337                        }
3338                        $jserr['status'] = 0;
3339
3340                        if ($mode == 1)
3341                        {
3342                                $jserr['content'] = self::compressHTML('<div class="margin">
3343                                                ${nopreview}
3344                                                <div class="failure_notice">
3345                                                        <h3>${error}</h3>
3346                                                        <p>mem usage: ' . number_format(memory_get_usage() / 1E6, 2) . ' MB : ' . number_format(memory_get_peak_usage() / 1E6, 2) . ' MB</p>
3347                                                        <p>' . $emsg . '</p>
3348                                                </div>
3349                                        </div>');       // <br/><button value="' . $url . '">${download}</button>
3350                        }
3351                }
3352        }
3353
3354
3355
3356
3357
3358
3359        public function getAllowedMimeTypes($mime_filter = null)
3360        {
3361                $mimeTypes = array();
3362
3363                if (empty($mime_filter)) return null;
3364                $mset = explode(',', $mime_filter);
3365                for($i = count($mset) - 1; $i >= 0; $i--)
3366                {
3367                        if (strpos($mset[$i], '/') === false) $mset[$i] .= '/';
3368                }
3369
3370                $mimes = $this->getMimeTypeDefinitions();
3371
3372                foreach ($mimes as $mime)
3373                {
3374                        foreach($mset as $filter)
3375                        {
3376                                if (FileManagerUtility::startsWith($mime, $filter))
3377                                        $mimeTypes[] = $mime;
3378                        }
3379                }
3380
3381                return $mimeTypes;
3382        }
3383
3384        public function getMimeTypeDefinitions()
3385        {
3386                static $mimes;
3387
3388                if (!$mimes) $mimes = parse_ini_file($this->options['mimeTypesPath']);
3389                if (!$mimes) $mimes = array(); // prevent faulty mimetype ini file from b0rking other code sections.
3390                return $mimes;
3391        }
3392
3393        public function IsAllowedMimeType($mime_type, $mime_filters)
3394        {
3395                if (empty($mime_type))
3396                        return false;
3397                if (!is_array($mime_filters))
3398                        return true;
3399
3400                return in_array($mime_type, $mime_filters);
3401        }
3402
3403        /**
3404         * Returns (if possible) the mimetype of the given file
3405         *
3406         * @param string $file
3407         * @param boolean $just_guess when TRUE, files are not 'sniffed' to derive their actual mimetype
3408         *                            but instead only the swift (and blunt) process of guestimating
3409         *                            the mime type from the file extension is performed.
3410         */
3411        public function getMimeType($file, $just_guess = false, $ext = NULL)
3412        {
3413                if (is_dir($file))
3414                        return 'text/directory';
3415
3416    if(!isset($ext)) // _FILES['tmp_name'] does not have an extention, we need to provide it   
3417                $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
3418
3419                $mime = null;
3420                $ini = error_reporting(0);
3421                if (function_exists('finfo_open') && $f = finfo_open(FILEINFO_MIME, getenv('MAGIC')))
3422                {
3423                        $mime = finfo_file($f, $file);
3424                        // some systems also produce the cracter encoding with the mime type; strip if off:
3425                        $ma = explode(';', $mime);
3426                        $mime = $ma[0];
3427                        finfo_close($f);
3428                }
3429                error_reporting($ini);
3430
3431                if (!$mime && !$just_guess && in_array($ext, array('gif', 'jpg', 'jpeg', 'png')))
3432                {
3433                        $image = @getimagesize($file);
3434                        if($image !== false && !empty($image['mime']))
3435                                $mime = $image['mime'];
3436                }
3437
3438                if ((!$mime || $mime == 'application/octet-stream') && strlen($ext) > 0)
3439                {
3440                        $ext2mimetype_arr = $this->getMimeTypeDefinitions();
3441
3442                        if (!empty($ext2mimetype_arr[$ext]))
3443                                return $ext2mimetype_arr[$ext];
3444                }
3445
3446                if (!$mime)
3447                        $mime = 'application/octet-stream';
3448
3449                return $mime;
3450        }
3451
3452
3453
3454
3455
3456        protected /* static */ function getGETparam($name, $default_value = null)
3457        {
3458                if (is_array($_GET) && !empty($_GET[$name]))
3459                {
3460                        $rv = $_GET[$name];
3461
3462                        // see if there's any stuff in there which we don't like
3463                        if (!preg_match('/[^A-Za-z0-9\/~!@#$%^&*()_+{}[]\'",.?]/', $rv))
3464                        {
3465                                return $rv;
3466                        }
3467                }
3468                return $default_value;
3469        }
3470
3471        protected /* static */ function getPOSTparam($name, $default_value = null)
3472        {
3473                if (is_array($_POST) && !empty($_POST[$name]))
3474                {
3475                        $rv = $_POST[$name];
3476
3477                        // see if there's any stuff in there which we don't like
3478                        if (!preg_match('/[^A-Za-z0-9\/~!@#$%^&*()_+{}[]\'",.?]/', $rv))
3479                        {
3480                                return $rv;
3481                        }
3482                }
3483                return $default_value;
3484        }
3485}
3486
3487
3488
3489
3490
3491
3492class FileManagerException extends Exception {}
3493
3494
3495
3496
3497
3498/* Stripped-down version of some Styx PHP Framework-Functionality bundled with this FileBrowser. Styx is located at: http://styx.og5.net */
3499class FileManagerUtility
3500{
3501        public static function endsWith($string, $look)
3502        {
3503                return strrpos($string, $look)===strlen($string)-strlen($look);
3504        }
3505
3506        public static function startsWith($string, $look)
3507        {
3508                return strpos($string, $look)===0;
3509        }
3510
3511
3512        /**
3513         * Cleanup and check against 'already known names' in optional $options array.
3514         * Return a uniquified name equal to or derived from the original ($data).
3515         *
3516         * First clean up the given name ($data): by default all characters not part of the
3517         * set [A-Za-z0-9_] are converted to an underscore '_'; series of these underscores
3518         * are reduced to a single one, and characters in the set [_.,&+ ] are stripped from
3519         * the lead and tail of the given name, e.g. '__name' would therefor be reduced to
3520         * 'name'.
3521         *
3522         * Next, check the now cleaned-up name $data against an optional set of names ($options array)
3523         * and return the name itself when it does not exist in the set,
3524         * otherwise return an augmented name such that it does not exist in the set
3525         * while having been constructed as name plus '_' plus an integer number,
3526         * starting at 1.
3527         *
3528         * Example:
3529         * If the set is {'file', 'file_1', 'file_3'} then $data = 'file' will return
3530         * the string 'file_2' instead, while $data = 'fileX' will return that same
3531         * value: 'fileX'.
3532         *
3533         * @param string $data     the name to be cleaned and checked/uniquified
3534         * @param array $options   an optional array of strings to check the given name $data against
3535         * @param string $extra_allowed_chars     optional set of additional characters which should pass
3536         *                                        unaltered through the cleanup stage. a dash '-' can be
3537         *                                        used to denote a character range, while the literal
3538         *                                        dash '-' itself, when included, should be positioned
3539         *                                        at the very start or end of the string.
3540         *
3541         *                                        Note that ] must NOT need to be escaped; we do this
3542         *                                        ourselves.
3543         * @param string $trim_chars              optional set of additional characters which are trimmed off the
3544         *                                        start and end of the name ($data); note that de dash
3545         *                                        '-' is always treated as a literal dash here; no
3546         *                                        range feature!
3547         *                                        The basic set of characters trimmed off the name is
3548         *                                        [. ]; this set cannot be reduced, only extended.
3549         *
3550         * @return cleaned-up and uniquified name derived from ($data).
3551         */
3552        public static function pagetitle($data, $options = null, $extra_allowed_chars = null, $trim_chars = null)
3553        {
3554                static $regex;
3555                if (!$regex){
3556                        $regex = array(
3557                                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;'),
3558                                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'),
3559                        );
3560                }
3561
3562                if (empty($data))
3563                                return $data;
3564
3565                // fixup $extra_allowed_chars to ensure it's suitable as a character sequence for a set in a regex:
3566                //
3567                // Note:
3568                //   caller must ensure a dash '-', when to be treated as a separate character, is at the very end of the string
3569                if (is_string($extra_allowed_chars))
3570                {
3571                        $extra_allowed_chars = str_replace(']', '\]', $extra_allowed_chars);
3572                        if (strpos($extra_allowed_chars, '-') === 0)
3573                        {
3574                                $extra_allowed_chars = substr($extra_allowed_chars, 1) . (strpos($extra_allowed_chars, '-') != strlen($extra_allowed_chars) - 1 ? '-' : '');
3575                        }
3576                }
3577                else
3578                {
3579                        $extra_allowed_chars = '';
3580                }
3581                // 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!
3582                $data = preg_replace('/[^A-Za-z0-9' . $extra_allowed_chars . ']+/', '_', str_replace($regex[0], $regex[1], $data));
3583                $data = trim($data, '_. ' . $trim_chars);
3584
3585                //$data = trim(substr(preg_replace('/(?:[^A-z0-9]|_|\^)+/i', '_', str_replace($regex[0], $regex[1], $data)), 0, 64), '_');
3586                return !empty($options) ? $this->checkTitle($data, $options) : $data;
3587        }
3588
3589        protected /* static */ function checkTitle($data, $options = array(), $i = 0)
3590        {
3591                if (!is_array($options)) return $data;
3592
3593                $lwr_data = strtolower($data);
3594
3595                foreach ($options as $content)
3596                        if ($content && strtolower($content) == $lwr_data . ($i ? '_' . $i : ''))
3597                                return $this->checkTitle($data, $options, ++$i);
3598
3599                return $data.($i ? '_' . $i : '');
3600        }
3601
3602        public static function isBinary($str)
3603        {
3604                for($i = 0; $i < strlen($str); $i++)
3605                {
3606                        $c = ord($str[$i]);
3607                        // do not accept ANY codes below SPACE, except TAB, CR and LF.
3608                        if ($c == 255 || ($c < 32 /* SPACE */ && $c != 9 && $c != 10 && $c != 13)) return true;
3609                }
3610
3611                return false;
3612        }
3613
3614        // helper function for rawurlencode_path(); as PHP < 5.3 lacks lambda functions
3615        protected static function __rawurlencode_path(&$value, $key)
3616        {
3617                $value = rawurlencode($value);
3618        }
3619
3620        /**
3621         * Apply rawurlencode() to each of the elements of the given path
3622         *
3623         * @note
3624         *   this method is provided as rawurlencode() tself also encodes the '/' separators in a path/string
3625         *   and we do NOT want to 'revert' such change with the risk of also picking up other %2F bits in
3626         *   the string (this assumes crafted paths can be fed to us).
3627         */
3628        public static function rawurlencode_path($path)
3629        {
3630                $encoded_path = explode('/', $path);
3631                array_walk($encoded_path, 'self::__rawurlencode_path');
3632                return implode('/', $encoded_path);
3633        }
3634
3635        /**
3636         * Convert a number (representing number of bytes) to a formatted string representing GB .. bytes,
3637         * depending on the size of the value.
3638         */
3639        public static function fmt_bytecount($val, $precision = 1)
3640        {
3641                $unit = array('TB', 'GB', 'MB', 'KB', 'bytes');
3642                for ($x = count($unit) - 1; $val >= 1024 && $x > 0; $x--)
3643                {
3644                        $val /= 1024.0;
3645                }
3646                $val = round($val, ($x > 0 ? $precision : 0));
3647                return $val . '&#160;' . $unit[$x];
3648        }
3649}
3650
Note: See TracBrowser for help on using the repository browser.