source: trunk/plugins/MootoolsFileManager/mootools-filemanager/Assets/Connector/FileManager.php @ 1327

Last change on this file since 1327 was 1327, checked in by gogo, 7 years ago

#1595 - suhosin caused breakage of MootoolsFileManager?, typical error session expired due to suhosin having killed the session, this works around it.

  • Property svn:executable set to *
File size: 197.7 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 & FileManagerWithAliasSupport 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 *   - thumbSmallSize: (integer) The (maximum) width / height in pixels of the thumb48 'small' thumbnails produced by this backend
30 *   - thumbBigSize: (integer) The (maximum) width / height in pixels of the thumb250 'big' thumbnails produced by this backend
31 *   - mimeTypesPath: (string, optional) The filesystem path to the MimeTypes.ini file. May exist in a place outside the DocumentRoot tree.
32 *   - dateFormat: (string, defaults to *j M Y - H:i*) The format in which dates should be displayed
33 *   - maxUploadSize: (integer, defaults to *20280000* bytes) The maximum file size for upload in bytes
34 *   - 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".
35 *   - 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)
36 *   - 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)
37 *   - 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)
38 *   - 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)
39 *   - 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)
40 *   - allowExtChange: (boolean, defaults to *false*) allow the file extension to be changed when performing a rename operation.
41 *   - safe: (boolean, defaults to *true*) If true, disallows 'exe', 'dll', 'php', 'php3', 'php4', 'php5', 'phps' and saves them as 'txt' instead.
42 *   - chmod: (integer, default is 0777) the permissions set to the uploaded files and created thumbnails (must have a leading "0", e.g. 0777)
43 *   - 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)
44 *   - showHiddenFoldersAndFiles: (boolean, defaults to *false*) whether or not to show 'dotted' directories and files -- such files are considered 'hidden' on UNIX file systems
45 *   - ViewIsAuthorized_cb (function/reference, default is *null*) authentication + authorization callback which can be used to determine whether the given directory may be viewed.
46 *     The parameter $action = 'view'.
47 *   - 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).
48 *     The parameter $action = 'detail'.
49 *   - UploadIsAuthorized_cb (function/reference, default is *null*) authentication + authorization callback which can be used to determine whether the given file may be uploaded.
50 *     The parameter $action = 'upload'.
51 *   - DownloadIsAuthorized_cb (function/reference, default is *null*) authentication + authorization callback which can be used to determine whether the given file may be downloaded.
52 *     The parameter $action = 'download'.
53 *   - CreateIsAuthorized_cb (function/reference, default is *null*) authentication + authorization callback which can be used to determine whether the given subdirectory may be created.
54 *     The parameter $action = 'create'.
55 *   - 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.
56 *     The parameter $action = 'destroy'.
57 *   - 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.
58 *     Note that currently support for copying subdirectories is missing.
59 *     The parameter $action = 'move'.
60 *     UploadIsComplete_cb (function/reference, default is *null*) Upload complete callback which can be used to post process a newly upload file.
61 *     The parameter $action = 'upload'.
62 *     DownloadIsComplete_cb (function/reference, default is *null*) Download complete callback which can be used to post process after a file has been downloaded file.
63 *     The parameter $action = 'download'.
64 *     DestroyIsComplete_cb (function/reference, default is *null*) Destroy complete callback which can be used to cleanup after a newly deleted/destroyed file.
65 *     The parameter $action = 'destroy'.
66 *
67 * Obsoleted options:
68 *   - 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.)
69 *
70 *
71 * About the action permissions (upload|destroy|create|move|download):
72 *
73 *     All the option "permissions" are set to FALSE by default. Developers should always SPECIFICALLY enable a permission to have that permission, for two reasons:
74 *
75 *     1. Developers forget to disable permissions, they don't forget to enable them (because things don't work!)
76 *
77 *     2. Having open permissions by default leaves potential for security vulnerabilities where those open permissions are exploited.
78 *
79 *
80 * For all authorization hooks (callback functions) the following applies:
81 *
82 *     The callback should return TRUE for yes (permission granted), FALSE for no (permission denied).
83 *     Parameters sent to the callback are:
84 *       ($this, $action, $fileinfo)
85 *     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.
86 *     $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.
87 *
88 *     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
89 *     Demos/FM-common.php, Demos/manager.php and Demos/selectImage.php
90 *
91 *
92 * Notes on relative paths and safety / security:
93 *
94 *   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,
95 *   i.e. dirname($_SERVER['SCRIPT_NAME']).
96 *
97 *   Requests may post/submit relative paths as arguments to their FileManager events/actions in $_GET/$_POST, and those relative paths will be
98 *   regarded as relative to the request URI handling script path, i.e. dirname($_SERVER['SCRIPT_NAME']) to make the most
99 *   sense from bother server and client coding perspective.
100 *
101 *
102 *   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
103 *   from succeeding. (An example of such would be an attacker posting his own 'destroy' event request requesting the destruction of
104 *   '../../../../../../../../../etc/passwd' for example. In more complex rigs, the attack may be assisted through attacks at these options' paths,
105 *   so these are subjected to the same scrutiny in here.)
106 *
107 *   All paths, absolute or relative, as passed to the event handlers (see the onXXX methods of this class) are ENFORCED TO ABIDE THE RULE
108 *   'every path resides within the options['directory'] a.k.a. BASEDIR rooted tree' without exception.
109 *   Because we can do without exceptions to important rules. ;-)
110 *
111 *   When paths apparently don't, they are coerced into adherence to this rule; when this fails, an exception is thrown internally and an error
112 *   will be reported and the action temrinated.
113 *
114 *  'LEGAL URL paths':
115 *
116 *   Paths which adhere to the aforementioned rule are so-called LEGAL URL paths; their 'root' equals BASEDIR.
117 *
118 *   BASEDIR equals the path pointed at by the options['directory'] setting. It is therefore imperative that you ensure this value is
119 *   correctly set up; worst case, this setting will equal DocumentRoot.
120 *   In other words: you'll never be able to reach any file or directory outside this site's DocumentRoot directory tree, ever.
121 *
122 *
123 *  Path transformations:
124 *
125 *   To allow arbitrary directory/path mapping algorithms to be applied (e.g. when implementing Alias support such as available in the
126 *   derived class FileManagerWithAliasSupport), all paths are, on every change/edit, transformed from their LEGAL URL representation to
127 *   their 'absolute URI path' (which is suitable to be used in links and references in HTML output) and 'absolute physical filesystem path'
128 *   equivalents.
129 *   By enforcing such a unidirectional transformation we implicitly support non-reversible and hard-to-reverse path aliasing mechanisms,
130 *   e.g. complex regex+context based path manipulations in the server.
131 *
132 *
133 *   When you need your paths to be restricted to the bounds of the options['directory'] tree (which is a subtree of the DocumentRoot based
134 *   tree), you may wish to use the 'legal' class of path transformation member functions:
135 *
136 *   - legal2abs_url_path()
137 *   - rel2abs_legal_url_path()
138 *   - legal_url_path2file_path()
139 *
140 *   When you have a 'absolute URI path' or a path relative in URI space (implicitly relative to dirname($_SERVER['SCRIPT_NAME']) ), you can
141 *   transform such a path to either a guaranteed-absolute URI space path or a filesystem path:
142 *
143 *   - rel2abs_url_path()
144 *   - url_path2file_path()
145 *
146 *   Any other path transformations are ILLEGAL and DANGEROUS. The only other possibly legal transformation is from absolute URI path to
147 *   BASEDIR-based LEGAL URL path, as the URI path space is assumed to be linear and contiguous. However, this operation is HIGHLY discouraged
148 *   as it is a very strong indicator of other faulty logic, so we do NOT offer a method for this.
149 *
150 *
151 * Hooks: Detailed Interface Specification:
152 *
153 *   All 'authorization' callback hooks share a common interface specification (function parameter set). This is by design, so one callback
154 *   function can be used to process any and all of these events:
155 *
156 *   Function prototype:
157 *
158 *       function CallbackFunction($mgr, $action, &$info)
159 *
160 *   where
161 *
162 *       $msg:      (object) reference to the current FileManager class instance. Can be used to invoke public FileManager methods inside
163 *                  the callback.
164 *
165 *       $action:   (string) identifies the event being processed. Can be one of these:
166 *
167 *                  'create'          create new directory
168 *                  'move'            move or copy a file or directory
169 *                  'destroy'         delete a file or directory
170 *                  'upload'          upload a single file (when performing a bulk upload, each file will be uploaded individually)
171 *                  'download'        download a file
172 *                  'view'            show a directory listing (in either 'list' or 'thumb' mode)
173 *                  'detail'          show detailed information about the file and, whn possible, provide a link to a (largish) thumbnail
174 *
175 *       $info      (array) carries all the details. Some of which can even be manipulated if your callbac is more than just an
176 *                  authentication / authorization checker. ;-)
177 *                  For more detail, see the next major section.
178 *
179 *   The callback should return a boolean, where TRUE means the session/client is authorized to execute the action, while FALSE
180 *   will cause the backend to report an authentication error and abort the action.
181 *
182 *  Exceptions throwing from the callback:
183 *
184 *   Note that you may choose to throw exceptions from inside the callback; those will be caught and transformed to proper error reports.
185 *
186 *   You may either throw any exceptions based on either the FileManagerException or Exception classes. When you format the exception
187 *   message as "XYZ:data", where 'XYZ' is a alphanumeric-only word, this will be transformed to a i18n-support string, where
188 *   'backend.XYZ' must map to a translation string (e.g. 'backend.nofile', see also the Language/Language.XX.js files) and the optional
189 *   'data' tail will be appended to the translated message.
190 *
191 *
192 * $info: the details:
193 *
194 *   Here is the list of $info members per $action event code:
195 *
196 *   'upload':
197 *
198 *           $info[] contains:
199 *
200 *               'legal_dir_url'         (string) LEGAL URI path to the directory where the file is being uploaded. You may invoke
201 *                                           $dir = $mgr->legal_url_path2file_path($legal_dir_url);
202 *                                       to obtain the physical filesystem path (also available in the 'dir' $info entry, by the way!), or
203 *                                           $dir_url = $mgr->legal2abs_url_path($legal_dir_url);
204 *                                       to obtain the absolute URI path for the given directory.
205 *
206 *               'dir'                   (string) physical filesystem path to the directory where the file is being uploaded.
207 *
208 *               'raw_filename'          (string) the raw, unprocessed filename of the file being being uploaded, as specified by the client.
209 *
210 *                                       WARNING: 'raw_filename' may contain anything illegal, such as directory paths instead of just a filename,
211 *                                                filesystem-illegal characters and what-not. Use 'name'+'extension' instead if you want to know
212 *                                                where the upload will end up.
213 *
214 *               'filename'              (string) the filename, plus extension, of the file being uploaded; this filename is ensured
215 *                                       to be both filesystem-legal, unique and not yet existing in the given directory.
216 *
217 *                                       Note that the file name extension has already been cleaned, including 'safe' mode processing,
218 *                                       i.e. any uploaded binary executable will have been assigned the extension '.txt' already, when
219 *                                       FileManager's options['safe'] is enabled.
220 *
221 *               'tmp_filepath'          (string) filesystem path pointing at the temporary storage location of the uploaded file: you can
222 *                                       access the file data available here to optionally validate the uploaded content.
223 *
224 *               'meta_data'             (array) the content sniffed infor as produced by getID3
225 *
226 *               'mime'                  (string) the mime type as sniffed from the file
227 *
228 *               'mime_filter'           (optional, string) mime filter as specified by the client: a comma-separated string containing
229 *                                       full or partial mime types, where a 'partial' mime types is the part of a mime type before
230 *                                       and including the slash, e.g. 'image/'
231 *
232 *               'mime_filters'          (optional, array of strings) the set of allowed mime types, derived from the 'mime_filter' setting.
233 *
234 *               'size'                  (integer) number of bytes of the uploaded file
235 *
236 *               'maxsize'               (integer) the configured maximum number of bytes for any single upload
237 *
238 *               'overwrite'             (boolean) FALSE: the uploaded file will not overwrite any existing file, it will fail instead.
239 *
240 *                                       Set to TRUE (and adjust the 'name' and 'extension' entries as you desire) when you wish to overwrite
241 *                                       an existing file.
242 *
243 *               'resize'                (boolean) TRUE: any uploaded images are resized to the configured maximum dimensions before they
244 *                                       are stored on disk.
245 *
246 *               'chmod'                 (integer) UNIX access rights (default: 0666) for the file-to-be-created (RW for user,group,world).
247 *
248 *                                       Note that the eXecutable bits have already been stripped before the callback was invoked.
249 *
250 *               'preliminary_json'      (array) the JSON data collected so far; when ['status']==1, then we're performing a regular upload
251 *                                       operation, when the ['status']==0, we are performing a defective upload operation.
252 *
253 *               'validation_failure'    (string) NULL: no validation error has been detected before the callback was invoked; non-NULL, e.g.
254 *                                       "nofile": the string passed as message parameter of the FileManagerException, which will be thrown
255 *                                       after the callback has returned. (You may alter the 'validation_failure' string value to change the
256 *                                       reported error, or set it to NULL to turn off the validation error report entirely -- we assume you
257 *                                       will have corrected the other fileinfo[] items as well, when resetting the validation error.
258 *
259 *
260 *         Note that this request originates from a Macromedia Flash client: hence you'll need to use the
261 *         $_POST[session_name()] value to manually set the PHP session_id() before you start your your session
262 *         again.
263 *
264 *         The frontend-specified options.propagateData items will be available as $_POST[] items.
265 *
266 *         The frontend-specified options.uploadAuthData items will be available as $_POST[] items.
267 *
268 *
269 *  'download':
270 *
271 *           $info[] contains:
272 *
273 *               'legal_url'             (string) LEGAL URI path to the file to be downloaded. You may invoke
274 *                                           $dir = $mgr->legal_url_path2file_path($legal_url);
275 *                                       to obtain the physical filesystem path (also available in the 'file' $info entry, by the way!), or
276 *                                           $url = $mgr->legal2abs_url_path($legal_url);
277 *                                       to obtain the absolute URI path for the given file.
278 *
279 *               'file'                  (string) physical filesystem path to the file being downloaded.
280 *
281 *               'meta_data'             (array) the content sniffed infor as produced by getID3
282 *
283 *               'mime'                  (string) the mime type as sniffed from the file
284 *
285 *               'mime_filter'           (optional, string) mime filter as specified by the client: a comma-separated string containing
286 *                                       full or partial mime types, where a 'partial' mime types is the part of a mime type before
287 *                                       and including the slash, e.g. 'image/'
288 *
289 *               'mime_filters'          (optional, array of strings) the set of allowed mime types, derived from the 'mime_filter' setting.
290 *
291 *               'validation_failure'    (string) NULL: no validation error has been detected before the callback was invoked; non-NULL, e.g.
292 *                                       "nofile": the string passed as message parameter of the FileManagerException, which will be thrown
293 *                                       after the callback has returned. (You may alter the 'validation_failure' string value to change the
294 *                                       reported error, or set it to NULL to turn off the validation error report entirely -- we assume you
295 *                                       will have corrected the other fileinfo[] items as well, when resetting the validation error.
296 *
297 *         The frontend-specified options.propagateData items will be available as $_POST[] items.
298 *
299 *
300 *  'create': // create directory
301 *
302 *           $info[] contains:
303 *
304 *               'legal_url'             (string) LEGAL URI path to the parent directory of the directory being created. You may invoke
305 *                                           $dir = $mgr->legal_url_path2file_path($legal_url);
306 *                                       to obtain the physical filesystem path (also available in the 'dir' $info entry, by the way!), or
307 *                                           $url = $mgr->legal2abs_url_path($legal_url);
308 *                                       to obtain the absolute URI path for this parent directory.
309 *
310 *               'dir'                   (string) physical filesystem path to the parent directory of the directory being created.
311 *
312 *               'raw_name'              (string) the name of the directory to be created, as specified by the client (unfiltered!)
313 *
314 *               'uniq_name'             (string) the name of the directory to be created, filtered and ensured to be both unique and
315 *                                       not-yet-existing in the filesystem.
316 *
317 *               'newdir'                (string) the filesystem absolute path to the directory to be created; identical to:
318 *                                           $newdir = $mgr->legal_url_path2file_path($legal_url . $uniq_name);
319 *                                       Note the above: all paths are transformed from URI space to physical disk every time a change occurs;
320 *                                       this allows us to map even not-existing 'directories' to possibly disparate filesystem locations.
321 *
322 *               'chmod'                 (integer) UNIX access rights (default: 0777) for the directory-to-be-created (RWX for user,group,world)
323 *
324 *               'preliminary_json'      (array) the JSON data collected so far; when ['status']==1, then we're performing a regular 'create'
325 *                                       operation, when the ['status']==0, we are performing a defective 'create' operation.
326 *
327 *               'validation_failure'    (string) NULL: no validation error has been detected before the callback was invoked; non-NULL, e.g.
328 *                                       "nofile": the string passed as message parameter of the FileManagerException, which will be thrown
329 *                                       after the callback has returned. (You may alter the 'validation_failure' string value to change the
330 *                                       reported error, or set it to NULL to turn off the validation error report entirely -- we assume you
331 *                                       will have corrected the other fileinfo[] items as well, when resetting the validation error.
332 *
333 *         The frontend-specified options.propagateData items will be available as $_POST[] items.
334 *
335 *
336 *  'destroy':
337 *
338 *           $info[] contains:
339 *
340 *               'legal_url'             (string) LEGAL URI path to the file/directory to be deleted. You may invoke
341 *                                           $dir = $mgr->legal_url_path2file_path($legal_url);
342 *                                       to obtain the physical filesystem path (also available in the 'file' $info entry, by the way!), or
343 *                                           $url = $mgr->legal2abs_url_path($legal_url);
344 *                                       to obtain the absolute URI path for the given file/directory.
345 *
346 *               'file'                  (string) physical filesystem path to the file/directory being deleted.
347 *
348 *               'meta_data'             (array) the content sniffed infor as produced by getID3
349 *
350 *               'mime'                  (string) the mime type as sniffed from the file / directory (directories are mime type: 'text/directory')
351 *
352 *               'mime_filter'           (optional, string) mime filter as specified by the client: a comma-separated string containing
353 *                                       full or partial mime types, where a 'partial' mime types is the part of a mime type before
354 *                                       and including the slash, e.g. 'image/'
355 *
356 *               'mime_filters'          (optional, array of strings) the set of allowed mime types, derived from the 'mime_filter' setting.
357 *
358 *                                       Note that the 'mime_filters', if any, are applied to the 'delete' operation in a special way: only
359 *                                       files matching one of the mime types in this list will be deleted; anything else will remain intact.
360 *                                       This can be used to selectively clean a directory tree.
361 *
362 *                                       The design idea behind this approach is that you are only allowed what you can see ('view'), so
363 *                                       all 'view' restrictions should equally to the 'delete' operation.
364 *
365 *               'preliminary_json'      (array) the JSON data collected so far; when ['status']==1, then we're performing a regular 'destroy'
366 *                                       operation, when the ['status']==0, we are performing a defective 'destroy' operation.
367 *
368 *               'validation_failure'    (string) NULL: no validation error has been detected before the callback was invoked; non-NULL, e.g.
369 *                                       "nofile": the string passed as message parameter of the FileManagerException, which will be thrown
370 *                                       after the callback has returned. (You may alter the 'validation_failure' string value to change the
371 *                                       reported error, or set it to NULL to turn off the validation error report entirely -- we assume you
372 *                                       will have corrected the other fileinfo[] items as well, when resetting the validation error.
373 *
374 *         The frontend-specified options.propagateData items will be available as $_POST[] items.
375 *
376 *
377 *  'move':  // move or copy!
378 *
379 *           $info[] contains:
380 *
381 *               'legal_url'             (string) LEGAL URI path to the source parent directory of the file/directory being moved/copied. You may invoke
382 *                                           $dir = $mgr->legal_url_path2file_path($legal_url);
383 *                                       to obtain the physical filesystem path (also available in the 'dir' $info entry, by the way!), or
384 *                                           $url = $mgr->legal2abs_url_path($legal_url);
385 *                                       to obtain the absolute URI path for the given directory.
386 *
387 *               'dir'                   (string) physical filesystem path to the source parent directory of the file/directory being moved/copied.
388 *
389 *               'path'                  (string) physical filesystem path to the file/directory being moved/copied itself; this is the full source path.
390 *
391 *               'name'                  (string) the name itself of the file/directory being moved/copied; this is the source name.
392 *
393 *               'legal_newurl'          (string) LEGAL URI path to the target parent directory of the file/directory being moved/copied. You may invoke
394 *                                           $dir = $mgr->legal_url_path2file_path($legal_url);
395 *                                       to obtain the physical filesystem path (also available in the 'dir' $info entry, by the way!), or
396 *                                           $url = $mgr->legal2abs_url_path($legal_url);
397 *                                       to obtain the absolute URI path for the given directory.
398 *
399 *               'newdir'                (string) physical filesystem path to the target parent directory of the file/directory being moved/copied;
400 *                                       this is the full path of the directory where the file/directory will be moved/copied to. (filesystem absolute)
401 *
402 *               'newpath'               (string) physical filesystem path to the target file/directory being moved/copied itself; this is the full destination path,
403 *                                       i.e. the full path of where the file/directory should be renamed/moved to. (filesystem absolute)
404 *
405 *               'newname'               (string) the target name itself of the file/directory being moved/copied; this is the destination name.
406 *
407 *                                       This filename is ensured to be both filesystem-legal, unique and not yet existing in the given target directory.
408 *
409 *               'rename'                (boolean) TRUE when a file/directory RENAME operation is requested (name change, staying within the same
410 *                                       parent directory). FALSE otherwise.
411 *
412 *               'is_dir'                (boolean) TRUE when the subject is a directory itself, FALSE when it is a regular file.
413 *
414 *               'function'              (string) PHP call which will perform the operation. ('rename' or 'copy')
415 *
416 *               'preliminary_json'      (array) the JSON data collected so far; when ['status']==1, then we're performing a regular 'move'
417 *                                       operation, when the ['status']==0, we are performing a defective 'move' operation.
418 *
419 *               'validation_failure'    (string) NULL: no validation error has been detected before the callback was invoked; non-NULL, e.g.
420 *                                       "nofile": the string passed as message parameter of the FileManagerException, which will be thrown
421 *                                       after the callback has returned. (You may alter the 'validation_failure' string value to change the
422 *                                       reported error, or set it to NULL to turn off the validation error report entirely -- we assume you
423 *                                       will have corrected the other fileinfo[] items as well, when resetting the validation error.
424 *
425 *         The frontend-specified options.propagateData items will be available as $_POST[] items.
426 *
427 *
428 *  'view':
429 *
430 *           $info[] contains:
431 *
432 *               'legal_url'             (string) LEGAL URI path to the directory being viewed/scanned. You may invoke
433 *                                           $dir = $mgr->legal_url_path2file_path($legal_url);
434 *                                       to obtain the physical filesystem path (also available in the 'dir' $info entry, by the way!), or
435 *                                           $url = $mgr->legal2abs_url_path($legal_url);
436 *                                       to obtain the absolute URI path for the scanned directory.
437 *
438 *               'dir'                   (string) physical filesystem path to the directory being viewed/scanned.
439 *
440 *               'collection'            (dual array of strings) arrays of files and directories (including '..' entry at the top when this is a
441 *                                       subdirectory of the FM-managed tree): only names, not full paths. The files array is located at the
442 *                                       ['files'] index, while the directories are available at the ['dirs'] index.
443 *
444 *               'meta_data'             (array) the content sniffed infor as produced by getID3
445 *
446 *               'mime_filter'           (optional, string) mime filter as specified by the client: a comma-separated string containing
447 *                                       full or partial mime types, where a 'partial' mime types is the part of a mime type before
448 *                                       and including the slash, e.g. 'image/'
449 *
450 *               'mime_filters'          (optional, array of strings) the set of allowed mime types, derived from the 'mime_filter' setting.
451 *
452 *               'file_preselect'        (optional, string) filename of a file in this directory which should be located and selected.
453 *                                       When found, the backend will provide an index number pointing at the corresponding JSON files[]
454 *                                       entry to assist the front-end in jumping to that particular item in the view.
455 *
456 *               'preliminary_json'      (array) the JSON data collected so far; when ['status']==1, then we're performing a regular view
457 *                                       operation (possibly as the second half of a copy/move/delete operation), when the ['status']==0,
458 *                                       we are performing a view operation as the second part of another otherwise failed action, e.g. a
459 *                                       failed 'create directory'.
460 *
461 *               'validation_failure'    (string) NULL: no validation error has been detected before the callback was invoked; non-NULL, e.g.
462 *                                       "nofile": the string passed as message parameter of the FileManagerException, which will be thrown
463 *                                       after the callback has returned. (You may alter the 'validation_failure' string value to change the
464 *                                       reported error, or set it to NULL to turn off the validation error report entirely -- we assume you
465 *                                       will have corrected the other fileinfo[] items as well, when resetting the validation error.
466 *
467 *         The frontend-specified options.propagateData items will be available as $_POST[] items.
468 *
469 *
470 *  'detail':
471 *
472 *           $info[] contains:
473 *
474 *               'legal_url'             (string) LEGAL URI path to the file/directory being inspected. You may invoke
475 *                                           $dir = $mgr->legal_url_path2file_path($legal_url);
476 *                                       to obtain the physical filesystem path (also available in the 'file' $info entry, by the way!), or
477 *                                           $url = $mgr->legal2abs_url_path($legal_url);
478 *                                       to obtain the absolute URI path for the given file.
479 *
480 *               'file'                  (string) physical filesystem path to the file being inspected.
481 *
482 *               'meta_data'             (array) the content sniffed infor as produced by getID3
483 *
484 *               'mime'                  (string) the mime type as sniffed from the file
485 *
486 *               'mime_filter'           (optional, string) mime filter as specified by the client: a comma-separated string containing
487 *                                       full or partial mime types, where a 'partial' mime types is the part of a mime type before
488 *                                       and including the slash, e.g. 'image/'
489 *
490 *               'mime_filters'          (optional, array of strings) the set of allowed mime types, derived from the 'mime_filter' setting.
491 *
492 *               'preliminary_json'      (array) the JSON data collected so far; when ['status']==1, then we're performing a regular 'detail'
493 *                                       operation, when the ['status']==0, we are performing a defective 'detail' operation.
494 *
495 *               'validation_failure'    (string) NULL: no validation error has been detected before the callback was invoked; non-NULL, e.g.
496 *                                       "nofile": the string passed as message parameter of the FileManagerException, which will be thrown
497 *                                       after the callback has returned. (You may alter the 'validation_failure' string value to change the
498 *                                       reported error, or set it to NULL to turn off the validation error report entirely -- we assume you
499 *                                       will have corrected the other fileinfo[] items as well, when resetting the validation error.
500 *
501 *         The frontend-specified options.propagateData items will be available as $_POST[] items.
502 *
503 *
504 *
505 * Developer Notes:
506 *
507 * - member functions which have a commented out 'static' keyword have it removed by design: it makes for easier overloading through
508 *   inheritance that way and meanwhile there's no pressing need to have those (public) member functions acccessible from the outside world
509 *   without having an instance of the FileManager class itself round at the same time.
510 */
511
512// ----------- compatibility checks ----------------------------------------------------------------------------
513if (version_compare(PHP_VERSION, '5.2.0') < 0)
514{
515        // die horribly: server does not match our requirements!
516        header('HTTP/1.0 500 FileManager requires PHP 5.2.0 or later', true, 500); // Internal server error
517        throw Exception('FileManager requires PHP 5.2.0 or later');   // this exception will most probably not be caught; that's our intent!
518}
519
520if (function_exists('UploadIsAuthenticated'))
521{
522        // die horribly: user has not upgraded his callback hook(s)!
523        header('HTTP/1.0 500 FileManager callback has not been upgraded!', true, 500); // Internal server error
524        throw Exception('FileManager callback has not been upgraded!');   // this exception will most probably not be caught; that's our intent!
525}
526
527if(!defined("I_KNOW_ABOUT_SUHOSIN") && ini_get('suhosin.session.cryptua'))
528{
529  header('HTTP/1.0 500 Developer must read https://github.com/sleemanj/mootools-filemanager/wiki/suhosin', true, 500); // Internal server error
530  throw Exception('suhosin.session.cryptua: https://github.com/sleemanj/mootools-filemanager/wiki/suhosin" }');   // this exception will most probably not be caught; that's our intent! 
531  exit;
532}
533//-------------------------------------------------------------------------------------------------------------
534
535if (!defined('DEVELOPMENT')) define('DEVELOPMENT', 0);   // make sure this #define is always known to us
536
537
538
539require_once(strtr(dirname(__FILE__), '\\', '/') . '/Tooling.php');
540require_once(strtr(dirname(__FILE__), '\\', '/') . '/Image.class.php');
541
542
543
544// the jpeg quality for the largest thumbnails (smaller ones are automatically done at increasingly higher quality)
545define('MTFM_THUMBNAIL_JPEG_QUALITY', 80);
546
547// the number of directory levels in the thumbnail cache; set to 2 when you expect to handle huge image collections.
548//
549// Note that each directory level distributes the files evenly across 256 directories; hence, you may set this
550// level count to 2 when you expect to handle more than 32K images in total -- as each image will have two thumbnails:
551// a 48px small one and a 250px large one.
552define('MTFM_NUMBER_OF_DIRLEVELS_FOR_CACHE', 1);
553
554// minimum number of cached getID3 results; cache is automatically pruned
555define('MTFM_MIN_GETID3_CACHESIZE', 16);
556
557// allow MTFM to use finfo_open() to help us produce mime types for files. This is slower than the basic file extension to mimetype mapping
558define('MTFM_USE_FINFO_OPEN', false);
559
560
561
562
563
564
565// flags for clean_ID3info_results()
566define('MTFM_CLEAN_ID3_STRIP_EMBEDDED_IMAGES',      0x0001);
567
568
569
570
571
572
573/**
574 * Cache element class custom-tailored for the MTFM: includes the code to construct a unique
575 * (thumbnail) cache filename and derive suitable cache filenames from the same template with
576 * minimal effort.
577 *
578 * Makes sure the generated (thumbpath) template is unique for each source file ('$legal_url'). We prevent
579 * reduced performance for large file sets: all thumbnails/templates derived from any files in the entire
580 * FileManager-managed directory tree, rooted by options['directory'], can become a huge collection,
581 * so we distribute them across a (thumbnail/cache) directory tree, which is created on demand.
582 *
583 * The thumbnails cache directory tree is determined by the MD5 of the full path to the source file ($legal_url),
584 * using the first two characters of the MD5, making for a span of 256 (directories).
585 *
586 * Note: when you expect to manage a really HUGE file collection from FM, you may dial up the
587 *       MTFM_NUMBER_OF_DIRLEVELS_FOR_CACHE define to 2 here.
588 */
589class MTFMCacheItem
590{
591        protected $store;
592
593        protected $legal_url;
594        protected $file;
595        protected $dirty;
596        protected $persistent_edits;
597        protected $loaded;
598        protected $fstat;
599
600        protected $cache_dir;
601        protected $cache_dir_mode;  // UNIX access bits: UGA:RWX
602        protected $cache_dir_url;
603        protected $cache_base;      // cache filename template base
604        protected $cache_tnext;     // thumbnail extension
605
606        protected $cache_file;
607
608        public function __construct($fm_obj, $legal_url, $prefetch = false, $persistent_edits = true)
609        {
610                $this->init($fm_obj, $legal_url, $prefetch, $persistent_edits);
611        }
612
613        public function init($fm_obj, $legal_url, $prefetch = false, $persistent_edits)
614        {
615                $this->dirty = false;
616                $this->persistent_edits = $persistent_edits;
617                $this->loaded = false;
618                $this->store = array();
619
620                $fmopts = $fm_obj->getSettings();
621
622                $this->legal_url = $legal_url;
623                $this->file = $fm_obj->legal_url_path2file_path($legal_url);
624                $this->fstat = null;
625
626                $fi = pathinfo($legal_url);
627                if (is_dir($this->file))
628                {
629                        $filename = $fi['basename'];
630                        unset($fi['extension']);
631                        $ext = '';
632                }
633                else
634                {
635                        $filename = $fi['filename'];
636                        $ext = strtolower((isset($fi['extension']) && strlen($fi['extension']) > 0) ? '.' . $fi['extension'] : '');
637                        switch ($ext)
638                        {
639                        case '.gif':
640                        case '.png':
641                        case '.jpg':
642                        case '.jpeg':
643                                break;
644
645                        case '.mp3':
646                                // default to JPG, as embedded images don't contain transparency info:
647                                $ext = '.jpg';
648                                break;
649
650                        default:
651                                //$ext = preg_replace('/[^A-Za-z0-9.]+/', '_', $ext);
652
653                                // default to PNG, as it'll handle transparancy and full color both:
654                                $ext = '.png';
655                                break;
656                        }
657                }
658
659                // as the cache file is generated, but NOT guaranteed from a safe filepath (FM may be visiting unsafe
660                // image files when they exist in a preloaded directory tree!) we do the full safe-filename transform
661                // on the name itself.
662                // The MD5 is taken from the untrammeled original, though:
663                $dircode = md5($legal_url);
664
665                $dir = '';
666                for ($i = 0; $i < MTFM_NUMBER_OF_DIRLEVELS_FOR_CACHE; $i++)
667                {
668                        $dir .= substr($dircode, 0, 2) . '/';
669                        $dircode = substr($dircode, 2);
670                }
671
672                $fn = substr($dircode, 0, 4) . '_' . preg_replace('/[^A-Za-z0-9]+/', '_', $filename);
673                $dircode = substr($dircode, 4);
674                $fn = substr($fn . $dircode, 0, 38);
675
676                $this->cache_dir_url = $fmopts['thumbnailPath'] . $dir;
677                $this->cache_dir = $fmopts['thumbnailCacheDir'] . $dir;
678                $this->cache_dir_mode = $fmopts['chmod'];
679                $this->cache_base = $fn;
680                $this->cache_tnext = $ext;
681
682                $cache_url = $fn . '-meta.nfo';
683                $this->cache_file = $this->cache_dir . $cache_url;
684
685                if ($prefetch)
686                {
687                        $this->load();
688                }
689        }
690
691        public function load()
692        {
693                if (!$this->loaded)
694                {
695                        $this->loaded = true; // always mark as loaded, even when the load fails
696
697                        if (!is_array($this->fstat) && file_exists($this->file))
698                        {
699                                $this->fstat = @stat($this->file);
700                        }
701                        if (file_exists($this->cache_file))
702                        {
703                                include($this->cache_file);  // unserialize();
704
705                                if (   isset($statdata) && isset($data) && is_array($data) && is_array($this->fstat) && is_Array($statdata)
706                                        && $statdata[10] == $this->fstat[10] // ctime
707                                        && $statdata[9]  == $this->fstat[9]   // mtime
708                                        && $statdata[7]  == $this->fstat[7]   // size
709                                   )
710                                {
711                                        if (!DEVELOPMENT)
712                                        {
713                                                // mix disk cache data with items already existing in RAM cache: we use a delayed-load scheme which necessitates this.
714                                                $this->store = array_merge($data, $this->store);
715                                        }
716                                }
717                                else
718                                {
719                                        // nuke disk cache!
720                                        @unlink($this->cache_file);
721                                }
722                        }
723                }
724        }
725
726        public function delete($every_ting_baby = false)
727        {
728                $rv = true;
729                $dir = $this->cache_dir;
730                $dir_exists = file_exists($dir);
731
732                // What do I get for ten dollars?
733                if ($every_ting_baby)
734                {
735                        if ($dir_exists)
736                        {
737                                $dir_and_mask = $dir . $this->cache_base . '*';
738                                $coll = safe_glob($dir_and_mask, GLOB_NODOTS | GLOB_NOSORT);
739
740                                if ($coll !== false)
741                                {
742                                        foreach($coll['files'] as $filename)
743                                        {
744                                                $file = $dir . $filename;
745                                                $rv &= @unlink($file);
746                                        }
747                                }
748                        }
749                }
750                else if (file_exists($this->cache_file))
751                {
752                        // nuke cache!
753                        $rv &= @unlink($this->cache_file);
754                }
755
756                // as the thumbnail subdirectory may now be entirely empty, try to remove it as well,
757                // but do NOT yack when we don't succeed: there may be other thumbnails, etc. in there still!
758                if ($dir_exists)
759                {
760                        for ($i = 0; $i < MTFM_NUMBER_OF_DIRLEVELS_FOR_CACHE; $i++)
761                        {
762                                @rmdir($dir);
763                                $dir = dirname($dir);
764                        }
765                }
766
767                // also clear the data cached in RAM:
768                $this->dirty = false;
769                $this->loaded = true;  // we know the cache file doesn't exist any longer, so don't bother trying to load it again later on!
770                $this->store = array();
771
772                return $rv;
773        }
774
775        public function __destruct()
776        {
777                if ($this->dirty && $this->persistent_edits)
778                {
779                        // store data to persistent storage:
780                        if (!$this->mkCacheDir() && !$this->loaded)
781                        {
782                                // fetch from disk before saving in order to ensure RAM cache is mixed with _existing_ _valid_ disk cache (RAM wins on individual items).
783                                $this->load();
784                        }
785
786                        if (!is_array($this->fstat) && file_exists($this->file))
787                        {
788                                $this->fstat = @stat($this->file);
789                        }
790
791                        $data = '<?php
792
793// legal URL: ' . $this->legal_url . '
794
795$statdata = ' . var_export($this->fstat, true) . ';
796
797$data = ' . var_export($this->store, true) . ';' . PHP_EOL;
798
799                        @file_put_contents($this->cache_file, $data);
800                }
801        }
802
803        /*
804         * @param boolean $persistent    (default: TRUE) TRUE when we should also check the persistent cache storage for this item/key
805         */
806        public function fetch($key, $persistent = true)
807        {
808                if (isset($this->store[$key]))
809                {
810                        return $this->store[$key];
811                }
812                else if ($persistent && !$this->loaded)
813                {
814                        // only fetch from disk when we ask for items which haven't been stored yet.
815                        $this->load();
816                        if (isset($this->store[$key]))
817                        {
818                                return $this->store[$key];
819                        }
820                }
821
822                return null;
823        }
824
825        /*
826         * @param boolean $persistent    (default: TRUE) TRUE when we should also store this item/key in the persistent cache storage
827         */
828        public function store($key, $value, $persistent = true)
829        {
830                if (isset($this->store[$key]))
831                {
832                        $persistent &= ($this->store[$key] !== $value); // only mark cache as dirty when we actully CHANGE the value stored in here!
833                }
834                $this->dirty |= ($persistent && $this->persistent_edits);
835                $this->store[$key] = $value;
836        }
837
838
839        public function getThumbPath($dimensions)
840        {
841                assert(!empty($dimensions));
842                return $this->cache_dir . $this->cache_base . '-' . $dimensions . $this->cache_tnext;
843        }
844
845        public function getThumbURL($dimensions)
846        {
847                assert(!empty($dimensions));
848                return $this->cache_dir_url . $this->cache_base . '-' . $dimensions . $this->cache_tnext;
849        }
850
851        public function mkCacheDir()
852        {
853                if (!is_dir($this->cache_dir))
854                {
855                        @mkdir($this->cache_dir, $this->cache_dir_mode, true);
856                        return true;
857                }
858                return false;
859        }
860
861        public function getMimeType()
862        {
863                if (!empty($this->store['mime_type']))
864                {
865                        return $this->store['mime_type'];
866                }
867                //$mime = $fm_obj->getMimeFromExt($file);
868                return null;
869        }
870}
871
872
873
874
875
876
877
878class MTFMCache
879{
880        protected $store;           // assoc. array: stores cached data
881        protected $store_ts;        // assoc. array: stores corresponding 'cache timestamps' for use by the LRU algorithm
882        protected $store_lru_ts;    // integer: current 'cache timestamp'
883        protected $min_cache_size;  // integer: minimum cache size limit (maximum is a statistical derivate of this one, about twice as large)
884
885        public function __construct($min_cache_size)
886        {
887                $this->store = array();
888                $this->store_ts = array();
889                // store_lru_ts stores a 'timestamp' counter to track LRU: 'timestamps' older than threshold are discarded when cache is full
890                $this->store_lru_ts = 0;
891                $this->min_cache_size = $min_cache_size;
892        }
893
894        /*
895         * Return a reference to the cache slot. When the cache slot did not exist before, it will be created, and
896         * the value stored in the slot will be NULL.
897         *
898         * You can store any arbitrary data in a cache slot: it doesn't have to be a MTFMCacheItem instance.
899         */
900        public function &pick($key, $fm_obj = null, $create_if_not_exist = true)
901        {
902                assert(!empty($key));
903
904                $age_limit = $this->store_lru_ts - $this->min_cache_size;
905
906                if (isset($this->store[$key]))
907                {
908                        // mark as LRU entry; only update the timestamp when it's rather old (age/2) to prevent
909                        // cache flushing due to hammering of a few entries:
910                        if ($this->store_ts[$key] < $age_limit + $this->min_cache_size / 2)
911                        {
912                                $this->store_ts[$key] = $this->store_lru_ts++;
913                        }
914                }
915                else if ($create_if_not_exist)
916                {
917                        // only start pruning when we run the risk of overflow. Heuristic: when we're at 50% fill rate, we can expect more requests to come in, so we start pruning already
918                        if (count($this->store_ts) >= $this->min_cache_size / 2)
919                        {
920                                /*
921                                 * Cleanup/cache size restriction algorithm:
922                                 *
923                                 * Randomly probe the cache and check whether the probe has a 'timestamp' older than the configured
924                                 * minimum required lifetime. When the probe is older, it is discarded from the cache.
925                                 *
926                                 * As the probe is assumed to be perfectly random, further assuming we've got a cache size of N,
927                                 * then the chance we pick a probe older then age A is (N - A) / N  -- picking any age X has a
928                                 * chance of 1/N as random implies flat distribution. Hitting any of the most recent A entries
929                                 * is A * 1/N, hence picking any older item is 1 - A/N == (N - A) / N
930                                 *
931                                 * This means the growth of the cache beyond the given age limit A is a logarithmic curve, but
932                                 * we like to have a guaranteed upper limit significantly below N = +Inf, so we probe the cache
933                                 * TWICE for each addition: given a cache size of 2N, one of these probes should, on average,
934                                 * be successful, thus removing one cache entry on average for a cache size of 2N. As we only
935                                 * add 1 item at the same time, the statistically expected bound of the cache will be 2N.
936                                 * As chances increase for both probes to be successful when cache size increases, the risk
937                                 * of a (very) large cache size at any point in time is dwindingly small, while cost is constant
938                                 * per cache transaction (insert + dual probe).
939                                 *
940                                 * This scheme is expected to be faster (thanks to log growth curve and linear insert/prune costs)
941                                 * than the usual where one keeps meticulous track of the entries and their age and entries are
942                                 * discarded in order, oldest first.
943                                 */
944                                $probe_index = array_rand($this->store_ts);
945                                if ($this->store_ts[$probe_index] < $age_limit)
946                                {
947                                        // discard antiquated entry:
948                                        unset($this->store_ts[$probe_index]);
949                                        unset($this->store[$probe_index]);
950                                }
951                                $probe_index = array_rand($this->store_ts);
952                                if ($this->store_ts[$probe_index] < $age_limit)
953                                {
954                                        // discard antiquated entry:
955                                        unset($this->store_ts[$probe_index]);
956                                        unset($this->store[$probe_index]);
957                                }
958                        }
959
960                        /*
961                         * add this slot (empty for now) to the cache. Only do this AFTER the pruning, so it won't risk being
962                         * picked by the random process in there. We _need_ this one right now. ;-)
963                         */
964                        $this->store[$key] = (!empty($fm_obj) ? new MTFMCacheItem($fm_obj, $key) : null);
965                        $this->store_ts[$key] = $this->store_lru_ts++;
966                }
967                else
968                {
969                        // do not clutter the cache; all we're probably after this time is the assistance of a MTFMCacheItem:
970                        // provide a dummy cache entry, nulled and all; we won't be saving the stored data, if any, anyhow.
971                        if (isset($this->store['!']) && !empty($fm_obj))
972                        {
973                                $this->store['!']->init($fm_obj, $key, false, false);
974                        }
975                        else
976                        {
977                                $this->store['!'] = (!empty($fm_obj) ? new MTFMCacheItem($fm_obj, $key, false, false) : null);
978                        }
979                        $this->store_ts['!'] = 0;
980                        $key = '!';
981                }
982
983                return $this->store[$key];
984        }
985}
986
987
988
989
990
991
992
993class FileManager
994{
995        protected $options;
996        protected $getid3;
997        protected $getid3_cache;
998        protected $icon_cache;              // cache the icon paths per size (large/small) and file extension
999
1000        protected $thumbnailCacheDir;
1001        protected $thumbnailCacheParentDir;  // assistant precalculated value for scandir/view
1002        protected $managedBaseDir;           // precalculated filesystem path eqv. of options['directory']
1003
1004        public function __construct($options)
1005        {
1006                $this->options = array_merge(array(
1007                        /*
1008                         * Note that all default paths as listed below are transformed to DocumentRoot-based paths
1009                         * through the getRealPath() invocations further below:
1010                         */
1011                        'directory' => null,                                                        // the root of the 'legal URI' directory tree, to be managed by MTFM. MUST be in the DocumentRoot tree.
1012                        'assetBasePath' => null,                                                    // may sit outside options['directory'] but MUST be in the DocumentRoot tree
1013                        'thumbnailPath' => null,                                                    // may sit outside options['directory'] but MUST be in the DocumentRoot tree
1014                        'thumbSmallSize' => 48,                                                     // Used for thumb48 creation
1015                        'thumbBigSize' => 250,                                                      // Used for thumb250 creation
1016                        'mimeTypesPath' => strtr(dirname(__FILE__), '\\', '/') . '/MimeTypes.ini',  // an absolute filesystem path anywhere; when relative, it will be assumed to be against options['RequestScriptURI']
1017                        'documentRootPath' => null,                                                 // an absolute filesystem path pointing at URI path '/'. Default: SERVER['DOCUMENT_ROOT']
1018                        'RequestScriptURI' => null,                                                                                                 // default is $_SERVER['SCRIPT_NAME']
1019                        'dateFormat' => 'j M Y - H:i',
1020                        'maxUploadSize' => 2600 * 2600 * 3,
1021                        // 'maxImageSize' => 99999,                                                 // OBSOLETED, replaced by 'suggestedMaxImageDimension'
1022                        'maxImageDimension' => array('width' => 1024, 'height' => 768),             // Allow to specify the "Resize Large Images" tolerance level.
1023                        'upload' => false,
1024                        'destroy' => false,
1025                        'create' => false,
1026                        'move' => false,
1027                        'download' => false,
1028                        /* ^^^ this last one is easily circumnavigated if it's about images: when you can view 'em, you can 'download' them anyway.
1029                         *     However, for other mime types which are not previewable / viewable 'in their full bluntal nugity' ;-) , this will
1030                         *     be a strong deterent.
1031                         *
1032                         *     Think Springer Verlag and PDFs, for instance. You can have 'em, but only /after/ you've ...
1033                         */
1034                        'allowExtChange' => false,
1035                        'safe' => true,
1036                        'filter' => null,
1037                        'chmod' => 0777,
1038                        'ViewIsAuthorized_cb' => null,
1039                        'DetailIsAuthorized_cb' => null,
1040                        'UploadIsAuthorized_cb' => null,
1041                        'DownloadIsAuthorized_cb' => null,
1042                        'CreateIsAuthorized_cb' => null,
1043                        'DestroyIsAuthorized_cb' => null,
1044                        'MoveIsAuthorized_cb' => null,
1045                       
1046                        'UploadIsComplete_cb' => null,
1047                        'DownloadIsComplete_cb' => null,
1048                        'DestroyIsComplete_cb' => null,
1049                       
1050                        'showHiddenFoldersAndFiles' => false,      // Hide dot dirs/files ?
1051                        'useGetID3IfAvailable' => true,
1052                        'enableXSendFile' => false,
1053                        'readme_file'          => '.readme.html', // If a directory contains a file of this name, the contents of this file
1054                                               //  will be returned in the ajax.  The MFM front end will display this in
1055                                               //  the preview area, provided there is nothing else to display there.
1056                                               //  Useful for displaying help text.  The file should only be an
1057                                               //  html snippet, not a complete html file.
1058                                               
1059                ), (is_array($options) ? $options : array()));
1060
1061                // transform the obsoleted/deprecated options:
1062                if (!empty($this->options['maxImageSize']) && $this->options['maxImageSize'] != 1024 && $this->options['maxImageDimension']['width'] == 1024 && $this->options['maxImageDimension']['height'] == 768)
1063                {
1064                        $this->options['maxImageDimension'] = array('width' => $this->options['maxImageSize'], 'height' => $this->options['maxImageSize']);
1065                }
1066
1067                $document_root_fspath = null;
1068                if (!empty($this->options['documentRootPath']))
1069                {
1070                        $document_root_fspath = realpath($this->options['documentRootPath']);
1071                }
1072                if (empty($document_root_fspath))
1073                {
1074                        $document_root_fspath = realpath($_SERVER['DOCUMENT_ROOT']);
1075                }
1076                $document_root_fspath = strtr($document_root_fspath, '\\', '/');
1077                $document_root_fspath = rtrim($document_root_fspath, '/');
1078                $this->options['documentRootPath'] = $document_root_fspath;
1079
1080                // apply default to RequestScriptURI:
1081                if (empty($this->options['RequestScriptURI']))
1082                {
1083                        $this->options['RequestScriptURI'] = $this->getRequestScriptURI();
1084                }
1085
1086                // only calculate the guestimated defaults when they are indeed required:
1087                if ($this->options['directory'] == null || $this->options['assetBasePath'] == null || $this->options['thumbnailPath'] == null)
1088                {
1089                        $my_path = @realpath(dirname(__FILE__));
1090                        $my_path = strtr($my_path, '\\', '/');
1091                        $my_path = self::enforceTrailingSlash($my_path);
1092                       
1093                        // we throw an Exception here because when these do not apply, the user should have specified all three these entries!
1094                        if (!FileManagerUtility::startsWith($my_path, $document_root_fspath))
1095                        {
1096                                throw new FileManagerException('nofile');
1097                        }
1098
1099                        $my_url_path = str_replace($document_root_fspath, '', $my_path);
1100
1101                        if ($this->options['directory'] == null)
1102                        {
1103                                $this->options['directory'] = $my_url_path . '../../Demos/Files/';
1104                        }
1105                        if ($this->options['assetBasePath'] == null)
1106                        {
1107                                $this->options['assetBasePath'] = $my_url_path . '../../Assets/';
1108                        }
1109                        if ($this->options['thumbnailPath'] == null)
1110                        {
1111                                $this->options['thumbnailPath'] = $my_url_path . '../../Assets/Thumbs/';
1112                        }
1113                }
1114
1115                /*
1116                 * make sure we start with a very predictable and LEGAL options['directory'] setting, so that the checks applied to the
1117                 * (possibly) user specified value for this bugger actually can check out okay AS LONG AS IT'S INSIDE the DocumentRoot-based
1118                 * directory tree:
1119                 */
1120                $this->options['directory'] = $this->rel2abs_url_path($this->options['directory'] . '/');
1121
1122                $this->managedBaseDir = $this->url_path2file_path($this->options['directory']);
1123
1124                // now that the correct options['directory'] has been set up, go and check/clean the other paths in the options[]:
1125
1126                $this->options['thumbnailPath'] = $this->rel2abs_url_path($this->options['thumbnailPath'] . '/');
1127                $this->thumbnailCacheDir = $this->url_path2file_path($this->options['thumbnailPath']);  // precalculate this value; safe as we can assume the entire cache dirtree maps 1:1 to filesystem.
1128                $this->thumbnailCacheParentDir = $this->url_path2file_path(self::getParentDir($this->options['thumbnailPath']));    // precalculate this value as well; used by scandir/view
1129
1130                $this->options['assetBasePath'] = $this->rel2abs_url_path($this->options['assetBasePath'] . '/');
1131
1132                $this->options['mimeTypesPath'] = @realpath($this->options['mimeTypesPath']);
1133                if (empty($this->options['mimeTypesPath']))
1134                {
1135                        throw new FileManagerException('nofile');
1136                }
1137                $this->options['mimeTypesPath'] = strtr($this->options['mimeTypesPath'], '\\', '/');
1138
1139                $this->getid3_cache = new MTFMCache(MTFM_MIN_GETID3_CACHESIZE);
1140
1141                $this->icon_cache = array(array(), array());
1142        }
1143
1144        /**
1145         * @return array the FileManager options and settings.
1146         */
1147        public function getSettings()
1148        {
1149                return array_merge(array(
1150                                'thumbnailCacheDir' => $this->thumbnailCacheDir,
1151                                'thumbnailCacheParentDir' => $this->thumbnailCacheParentDir,
1152                                'managedBaseDir' => $this->managedBaseDir
1153                ), $this->options);
1154        }
1155
1156
1157
1158
1159        /**
1160         * Central entry point for any client side request.
1161         */
1162        public function fireEvent($event = null)
1163        {
1164                $event = (!empty($event) ? 'on' . ucfirst($event) : null);
1165                if (!$event || !method_exists($this, $event)) $event = 'onView';
1166
1167                $this->{$event}();
1168        }
1169
1170
1171
1172
1173
1174
1175        /**
1176         * Generalized 'view' handler, which produces a directory listing.
1177         *
1178         * Return the directory listing in a nested array, suitable for JSON encoding.
1179         */
1180        protected function _onView($legal_url, $json, $mime_filter, $file_preselect_arg = null, $filemask = '*')
1181        {
1182                $v_ex_code = 'nofile';
1183
1184                $dir = $this->legal_url_path2file_path($legal_url);
1185                $doubledot = null;
1186                $coll = null;
1187                if (is_dir($dir))
1188                {
1189                        /*
1190                         * Caching notice:
1191                         *
1192                         * Testing on Win7/64 has revealed that at least on that platform, directories' 'last modified' timestamp does NOT change when
1193                         * the contents of the directory are altered (e.g. when a file was added), hence filemtime() cannot be used for directories
1194                         * to detect any change and thus steer the cache access.
1195                         *
1196                         * When one assumes that all file access in the managed directory tree is performed through an MTFM entity, then we can use a
1197                         * different tactic (which, due to this risky assumption is dupped part of the group of 'aggressive caching' actions) where
1198                         * we check for the existence of a cache file for the given directory; when it does exist, we can use it.
1199                         * Also, when any editing activity occurs in a directory, we can either choose to update the dir-cache file (costly, tough,
1200                         * rather complex) or simply delete the dir-cache file to signal the next occurrence of the 'view' a fresh dirscan is
1201                         * required.
1202                         *
1203                         * Also, we can keep track of the completed thumbnail generation per file in this dir-cache file. However, the argument against
1204                         * such relative sophitication (to prevent a double round-trip per thumbnail in 'thumb' list view) is the heavy cost of
1205                         * loading + saving the (edited) dir-cache file for each thumbnail production. The question here is: are those costs significantly
1206                         * less then the cost of dirscan + round trips (or 'direct' mode thumbnail file tests) for each 'view' request? How many 'view's
1207                         * do you expect compared to the number of directory edits? 'Usually' that ratio should be rather high (few edits, many views),
1208                         * thus suggesting a benefit to this aggressive caching and cache updating for thumbnail production.    The 'cheaper for the
1209                         * thumbnail production' approach would be to consider it a 'directory edit' and thus nuke the dir-cache for every thumbnail (48px)
1210                         * produced. This is /probably/ slower than the cahce updating, as the latter requires only a single file access per 'view'
1211                         * operation; all we need to store are a flag (Y/N) per file in the directory, so the store size would be small, even for large
1212                         * directories.
1213                         *
1214                         * What to do? We haven't come to a decision yet.
1215                         *
1216                         * Code: TODO
1217                         */
1218
1219                        $coll = $this->scandir($dir, $filemask, false, 0, ($this->options['showHiddenFoldersAndFiles'] ? ~GLOB_NOHIDDEN : ~0));
1220                        if ($coll !== false)
1221                        {
1222                                /*
1223                                 * To ensure '..' ends up at the very top of the view, no matter what the other entries in $coll['dirs'][] are made of,
1224                                 * we pop the last element off the array, check whether it's the double-dot, and if so, keep it out while we
1225                                 * let the sort run.
1226                                 */
1227                                $doubledot = array_pop($coll['dirs']);
1228                                if ($doubledot !== null && $doubledot !== '..')
1229                                {
1230                                        $coll['dirs'][] = $doubledot;
1231                                        $doubledot = null;
1232                                }
1233                                natcasesort($coll['dirs']);
1234                                natcasesort($coll['files']);
1235
1236                                $v_ex_code = null;
1237                        }
1238                }
1239
1240                $mime_filters = $this->getAllowedMimeTypes($mime_filter);
1241
1242                $fileinfo = array(
1243                                'legal_url' => $legal_url,
1244                                'dir' => $dir,
1245                                'collection' => $coll,
1246                                'mime_filter' => $mime_filter,
1247                                'mime_filters' => $mime_filters,
1248                                'file_preselect' => $file_preselect_arg,
1249                                'preliminary_json' => $json,
1250                                'validation_failure' => $v_ex_code
1251                        );
1252
1253                if (!empty($this->options['ViewIsAuthorized_cb']) && function_exists($this->options['ViewIsAuthorized_cb']) && !$this->options['ViewIsAuthorized_cb']($this, 'view', $fileinfo))
1254                {
1255                        $v_ex_code = $fileinfo['validation_failure'];
1256                        if (empty($v_ex_code)) $v_ex_code = 'authorized';
1257                }
1258                if (!empty($v_ex_code))
1259                        throw new FileManagerException($v_ex_code);
1260
1261                $legal_url = $fileinfo['legal_url'];
1262                $dir = $fileinfo['dir'];
1263                $coll = $fileinfo['collection'];
1264                $mime_filter = $fileinfo['mime_filter'];
1265                $mime_filters = $fileinfo['mime_filters'];
1266                $file_preselect_arg = $fileinfo['file_preselect'];
1267                $json = $fileinfo['preliminary_json'];
1268
1269                $file_preselect_index = -1;
1270                $out = array(array(), array());
1271
1272                $mime = 'text/directory';
1273                $iconspec = false;
1274
1275                if ($doubledot !== null)
1276                {
1277                        $filename = '..';
1278
1279                        $l_url = $legal_url . $filename;
1280
1281                        // must transform here so alias/etc. expansions inside legal_url_path2file_path() get a chance:
1282                        $file = $this->legal_url_path2file_path($l_url);
1283
1284                        $iconspec = 'is.directory_up';
1285
1286                        $icon48 = $this->getIcon($iconspec, false);
1287                        $icon48_e = FileManagerUtility::rawurlencode_path($icon48);
1288
1289                        $icon = $this->getIcon($iconspec, true);
1290                        $icon_e = FileManagerUtility::rawurlencode_path($icon);
1291
1292                        $out[1][] = array(
1293                                        'path' => $l_url,
1294                                        'name' => $filename,
1295                                        'mime' => $mime,
1296                                        'icon48' => $icon48_e,
1297                                        'icon' => $icon_e
1298                                );
1299                }
1300
1301                // now precalc the directory-common items (a.k.a. invariant computation / common subexpression hoisting)
1302                $iconspec_d = 'is.directory';
1303
1304                $icon48_d = $this->getIcon($iconspec_d, false);
1305                $icon48_de = FileManagerUtility::rawurlencode_path($icon48_d);
1306
1307                $icon_d = $this->getIcon($iconspec_d, true);
1308                $icon_de = FileManagerUtility::rawurlencode_path($icon_d);
1309
1310                foreach ($coll['dirs'] as $filename)
1311                {
1312                        $l_url = $legal_url . $filename;
1313
1314                        $out[1][] = array(
1315                                        'path' => $l_url,
1316                                        'name' => $filename,
1317                                        'mime' => $mime,
1318                                        'icon48' => $icon48_de,
1319                                        'icon' => $icon_de
1320                                );
1321                }
1322
1323                // and now list the files in the directory
1324                $idx = 0;
1325                foreach ($coll['files'] as $filename)
1326                {
1327                        $l_url = $legal_url . $filename;
1328
1329                        // Do not allow the getFileInfo()/imageinfo() overhead per file for very large directories; just guess the mimetype from the filename alone.
1330                        // The real mimetype will show up in the 'details' view anyway as we'll have called getFileInfo() by then!
1331                        $mime = $this->getMimeFromExt($filename);
1332                        $iconspec = $filename;
1333
1334                        if (!$this->IsAllowedMimeType($mime, $mime_filters))
1335                                continue;
1336
1337                        if ($filename === $file_preselect_arg)
1338                        {
1339                                $file_preselect_index = $idx;
1340                        }
1341
1342                        /*
1343                         * offload the thumbnailing process to another event ('event=detail / mode=direct') to be fired by the client
1344                         * when it's time to render the thumbnail: the offloading helps us tremendously in coping with large
1345                         * directories:
1346                         * WE simply assume the thumbnail will be there, so we don't even need to check for its existence
1347                         * (which saves us one more file_exists() per item at the very least). And when it doesn't, that's
1348                         * for the event=thumbnail handler to worry about (creating the thumbnail on demand or serving
1349                         * a generic icon image instead).
1350                         *
1351                         * For now, simply assign a basic icon to any and all; the 'detail' event will replace this item in the frontend
1352                         * when the time has arrives when that 'detail' request has been answered.
1353                         */
1354                        $icon48 = $this->getIcon($iconspec, false);
1355                        $icon48_e = FileManagerUtility::rawurlencode_path($icon48);
1356
1357                        $icon = $this->getIcon($iconspec, true);
1358                        $icon_e = FileManagerUtility::rawurlencode_path($icon);
1359
1360                        $out[0][] = array(
1361                                        'path' => $l_url,
1362                                        'name' => $filename,
1363                                        'mime' => $mime,
1364                                        // we don't know the thumbnail paths yet --> this will trigger deferred requests: (event=detail, mode=direct)
1365                                        'thumbs_deferred' => true,
1366                                        'icon48' => $icon48_e,
1367                                        'icon' => $icon_e
1368                                );
1369                        $idx++;
1370                }
1371
1372                return array_merge((is_array($json) ? $json : array()), array(
1373                                'root' => substr($this->options['directory'], 1),
1374                                'this_dir' => array(
1375                                        'path' => $legal_url,
1376                                        'name' => basename($legal_url),
1377                                        'date' => date($this->options['dateFormat'], @filemtime($dir)),
1378                                        'mime' => 'text/directory',
1379                                        'icon48' => $icon48_de,
1380                                        'icon' => $icon_de,
1381                                        'readme' => file_exists($dir . '/' . $this->options['readme_file'])
1382                  ? file_get_contents($dir . '/' . $this->options['readme_file'])
1383                  : null,
1384                                ),
1385                                'preselect_index' => ($file_preselect_index >= 0 ? $file_preselect_index + count($out[1]) + 1 : 0),
1386                                'preselect_name' => ($file_preselect_index >= 0 ? $file_preselect_arg : null),
1387                                'dirs' => $out[1],
1388                                'files' => $out[0]
1389                        ));
1390        }
1391
1392        /**
1393         * Process the 'view' event (default event fired by fireEvent() method)
1394         *
1395         * Returns a JSON encoded directory view list.
1396         *
1397         * Expected parameters:
1398         *
1399         * $_POST['directory']     path relative to basedir a.k.a. options['directory'] root
1400         *
1401         * $_POST['file_preselect']     optional filename or path:
1402         *                         when a filename, this is the filename of a file in this directory
1403         *                         which should be located and selected. When found, the backend will
1404         *                         provide an index number pointing at the corresponding JSON files[]
1405         *                         entry to assist the front-end in jumping to that particular item
1406         *                         in the view.
1407         *
1408         *                         when a path, it is either an absolute or a relative path:
1409         *                         either is assumed to be a URI URI path, i.e. rooted at
1410         *                           DocumentRoot.
1411         *                         The path will be transformed to a LEGAL URI path and
1412         *                         will OVERRIDE the $_POST['directory'] path.
1413         *                         Otherwise, this mode acts as when only a filename was specified here.
1414         *                         This mode is useful to help a frontend to quickly jump to a file
1415         *                         pointed at by a URI.
1416         *
1417         *                         N.B.: This also the only entry which accepts absolute URI paths and
1418         *                               transforms them to LEGAL URI paths.
1419         *
1420         *                         When the specified path is illegal, i.e. does not reside inside the
1421         *                         options['directory']-rooted LEGAL URI subtree, it will be discarded
1422         *                         entirely (as all file paths, whether they are absolute or relative,
1423         *                         must end up inside the options['directory']-rooted subtree to be
1424         *                         considered manageable files) and the process will continue as if
1425         *                         the $_POST['file_preselect'] entry had not been set.
1426         *
1427         * $_POST['filter']        optional mimetype filter string, amy be the part up to and
1428         *                         including the slash '/' or the full mimetype. Only files
1429         *                         matching this (set of) mimetypes will be listed.
1430         *                         Examples: 'image/' or 'application/zip'
1431         *
1432         * Errors will produce a JSON encoded error report, including at least two fields:
1433         *
1434         * status                  0 for error; nonzero for success
1435         *
1436         * error                   error message
1437         *
1438         * Next to these, the JSON encoded output will, with high probability, include a
1439         * list view of a valid parent or 'basedir' as a fast and easy fallback mechanism for client side
1440         * viewing code, jumping back to a existing directory. However, severe and repetitive errors may not produce this
1441         * 'fallback view list' so proper client code should check the 'status' field in the
1442         * JSON output.
1443         */
1444        protected function onView()
1445        {
1446                // try to produce the view; if it b0rks, retry with the parent, until we've arrived at the basedir:
1447                // then we fail more severely.
1448
1449                $emsg = null;
1450                $jserr = array(
1451                                'status' => 1
1452                        );
1453
1454                $mime_filter = $this->getPOSTparam('filter', $this->options['filter']);
1455                $legal_url = null;
1456
1457                try
1458                {
1459                        $dir_arg = $this->getPOSTparam('directory');
1460                        $legal_url = $this->rel2abs_legal_url_path($dir_arg . '/');
1461
1462                        $file_preselect_arg = $this->getPOSTparam('file_preselect');
1463                        try
1464                        {
1465                                if (!empty($file_preselect_arg))
1466                                {
1467                                        // check if this a path instead of just a basename, then convert to legal_url and split across filename and directory.
1468                                        if (strpos($file_preselect_arg, '/') !== false)
1469                                        {
1470                                                // this will also convert a relative path to an absolute path before transforming it to a LEGAL URI path:
1471                                                $legal_presel = $this->abs2legal_url_path($file_preselect_arg);
1472
1473                                                $prseli = pathinfo($legal_presel);
1474                                                $file_preselect_arg = $prseli['basename'];
1475                                                // override the directory!
1476                                                $legal_url = $prseli['dirname'];
1477                                                $legal_url = self::enforceTrailingSlash($legal_url);
1478                                        }
1479                                        else
1480                                        {
1481                                                $file_preselect_arg = basename($file_preselect_arg);
1482                                        }
1483                                }
1484                        }
1485                        catch(FileManagerException $e)
1486                        {
1487                                // discard the preselect input entirely:
1488                                $file_preselect_arg = null;
1489                        }
1490                }
1491                catch(FileManagerException $e)
1492                {
1493                        $emsg = $e->getMessage();
1494                        $legal_url = '/';
1495                        $file_preselect_arg = null;
1496                }
1497                catch(Exception $e)
1498                {
1499                        // catching other severe failures; since this can be anything it may not be a translation keyword in the message...
1500                        $emsg = $e->getMessage();
1501                        $legal_url = '/';
1502                        $file_preselect_arg = null;
1503                }
1504
1505                // loop until we drop below the bottomdir; meanwhile getDir() above guarantees that $dir is a subdir of bottomdir, hence dir >= bottomdir.
1506                $original_legal_url = $legal_url;
1507                do
1508                {
1509                        try
1510                        {
1511                                $rv = $this->_onView($legal_url, $jserr, $mime_filter, $file_preselect_arg);
1512
1513                                $this->sendHttpHeaders('Content-Type: application/json');
1514
1515                                echo json_encode($rv);
1516                                return;
1517                        }
1518                        catch(FileManagerException $e)
1519                        {
1520                                if ($emsg === null)
1521                                        $emsg = $e->getMessage();
1522                        }
1523                        catch(Exception $e)
1524                        {
1525                                // catching other severe failures; since this can be anything it may not be a translation keyword in the message...
1526                                if ($emsg === null)
1527                                        $emsg = $e->getMessage();
1528                        }
1529
1530                        // step down to the parent dir and retry:
1531                        $legal_url = self::getParentDir($legal_url);
1532                        $file_preselect_arg = null;
1533
1534                        $jserr['status']++;
1535
1536                } while ($legal_url !== false);
1537
1538                $this->modify_json4exception($jserr, $emsg, 'path = ' . $original_legal_url);
1539
1540                $this->sendHttpHeaders('Content-Type: application/json');
1541
1542                // when we fail here, it's pretty darn bad and nothing to it.
1543                // just push the error JSON and go.
1544                echo json_encode($jserr);
1545        }
1546
1547        /**
1548         * Process the 'detail' event
1549         *
1550         * Returns a JSON encoded HTML chunk describing the specified file (metadata such
1551         * as size, format and possibly a thumbnail image as well)
1552         *
1553         * Expected parameters:
1554         *
1555         * $_POST['directory']     path relative to basedir a.k.a. options['directory'] root
1556         *
1557         * $_POST['file']          filename (including extension, of course) of the file to
1558         *                         be detailed.
1559         *
1560         * $_POST['filter']        optional mimetype filter string, amy be the part up to and
1561         *                         including the slash '/' or the full mimetype. Only files
1562         *                         matching this (set of) mimetypes will be listed.
1563         *                         Examples: 'image/' or 'application/zip'
1564         *
1565         * $_POST['mode']          'auto' or 'direct': in 'direct' mode, all thumbnails are
1566         *                         forcibly generated _right_ _now_ as the client, using this
1567         *                         mode, tells us delayed generating and loading of the
1568         *                         thumbnail image(s) is out of the question.
1569         *                         'auto' mode will simply provide direct thumbnail image
1570         *                         URLs when those are available in cache, while 'auto' mode
1571         *                         will neglect to provide those, expecting the frontend to
1572         *                         delay-load them through another 'event=detail / mode=direct'
1573         *                         request later on.
1574         *                         'metaHTML': show the metadata as extra HTML content in
1575         *                         the preview pane (you can also turn that off using CSS:
1576         *                             div.filemanager div.filemanager-diag-dump
1577         *                             {
1578         *                                 display: none;
1579         *                             }
1580         *                         'metaJSON': deliver the extra getID3 metadata in JSON format
1581         *                         in the json['metadata'] field.
1582         *
1583         *                         Modes can be mixed by adding a '+' between them.
1584         *
1585         * Errors will produce a JSON encoded error report, including at least two fields:
1586         *
1587         * status                  0 for error; nonzero for success
1588         *
1589         * error                   error message
1590         */
1591        protected function onDetail()
1592        {
1593                $emsg = null;
1594                $legal_url = null;
1595                $file_arg = null;
1596                $jserr = array(
1597                                'status' => 1
1598                        );
1599
1600                try
1601                {
1602                        $v_ex_code = 'nofile';
1603
1604                        $mode = $this->getPOSTparam('mode');
1605                        $mode = explode('+', $mode);
1606                        if (empty($mode))
1607                        {
1608                                $mode = array();
1609                        }
1610
1611                        $file_arg = $this->getPOSTparam('file');
1612
1613                        $dir_arg = $this->getPOSTparam('directory');
1614                        $legal_url = $this->rel2abs_legal_url_path($dir_arg . '/');
1615
1616                        $mime_filter = $this->getPOSTparam('filter', $this->options['filter']);
1617                        $mime_filters = $this->getAllowedMimeTypes($mime_filter);
1618
1619                        $filename = null;
1620                        $file = null;
1621                        $mime = null;
1622                        $meta = null;
1623                        if (!empty($file_arg))
1624                        {
1625                                $filename = basename($file_arg);
1626                                // must normalize the combo as the user CAN legitimally request filename == '.' (directory detail view) for this event!
1627                                $path = $this->rel2abs_legal_url_path($legal_url . $filename);
1628                                //echo " path = $path, ($legal_url . $filename);\n";
1629                                $legal_url = $path;
1630                                // must transform here so alias/etc. expansions inside legal_url_path2file_path() get a chance:
1631                                $file = $this->legal_url_path2file_path($legal_url);
1632
1633                                if (is_readable($file))
1634                                {
1635                                        if (is_file($file))
1636                                        {
1637                                                $meta = $this->getFileInfo($file, $legal_url);
1638                                                $mime = $meta->getMimeType();
1639                                                if (!$this->IsAllowedMimeType($mime, $mime_filters))
1640                                                {
1641                                                        $v_ex_code = 'extension';
1642                                                }
1643                                                else
1644                                                {
1645                                                        $v_ex_code = null;
1646                                                }
1647                                        }
1648                                        else if (is_dir($file))
1649                                        {
1650                                                $mime = 'text/directory';
1651                                                $v_ex_code = null;
1652                                        }
1653                                }
1654                        }
1655
1656                        $fileinfo = array(
1657                                        'legal_url' => $legal_url,
1658                                        'file' => $file,
1659                                        'mode' => $mode,
1660                                        'meta_data' => $meta,
1661                                        'mime' => $mime,
1662                                        'mime_filter' => $mime_filter,
1663                                        'mime_filters' => $mime_filters,
1664                                        'preliminary_json' => $jserr,
1665                                        'validation_failure' => $v_ex_code
1666                                );
1667
1668                        if (!empty($this->options['DetailIsAuthorized_cb']) && function_exists($this->options['DetailIsAuthorized_cb']) && !$this->options['DetailIsAuthorized_cb']($this, 'detail', $fileinfo))
1669                        {
1670                                $v_ex_code = $fileinfo['validation_failure'];
1671                                if (empty($v_ex_code)) $v_ex_code = 'authorized';
1672                        }
1673                        if (!empty($v_ex_code))
1674                                throw new FileManagerException($v_ex_code);
1675
1676                        $legal_url = $fileinfo['legal_url'];
1677                        //$file = $fileinfo['file'];
1678                        $mode = $fileinfo['mode'];
1679                        $meta = $fileinfo['meta_data'];
1680                        //$mime = $fileinfo['mime'];
1681                        $mime_filter = $fileinfo['mime_filter'];
1682                        $mime_filters = $fileinfo['mime_filters'];
1683                        $jserr = $fileinfo['preliminary_json'];
1684
1685                        $jserr = $this->extractDetailInfo($jserr, $legal_url, $meta, $mime_filter, $mime_filters, $mode);
1686
1687                        $this->sendHttpHeaders('Content-Type: application/json');
1688
1689                        echo json_encode($jserr);
1690                        return;
1691                }
1692                catch(FileManagerException $e)
1693                {
1694                        $emsg = $e->getMessage();
1695                }
1696                catch(Exception $e)
1697                {
1698                        // catching other severe failures; since this can be anything and should only happen in the direst of circumstances, we don't bother translating
1699                        $emsg = $e->getMessage();
1700                }
1701
1702                $this->modify_json4exception($jserr, $emsg, 'file = ' . $file_arg . ', path = ' . $legal_url);
1703
1704                $icon48 = $this->getIconForError($emsg, 'is.default-error', false);
1705                $icon48_e = FileManagerUtility::rawurlencode_path($icon48);
1706                $icon = $this->getIconForError($emsg, 'is.default-error', true);
1707                $icon_e = FileManagerUtility::rawurlencode_path($icon);
1708                $jserr['thumb250'] = null;
1709                $jserr['thumb48'] = null;
1710                $jserr['icon48'] = $icon48_e;
1711                $jserr['icon'] = $icon_e;
1712
1713                $postdiag_err_HTML = '<p class="err_info">' . $emsg . '</p>';
1714                $preview_HTML = '${nopreview}';
1715                $content = '';
1716                //$content .= '<h3>${preview}</h3>';
1717                $content .= '<div class="filemanager-preview-content">' . $preview_HTML . '</div>';
1718                //$content .= '<h3>Diagnostics</h3>';
1719                //$content .= '<div class="filemanager-detail-diag">;
1720                $content .= '<div class="filemanager-errors">' . $postdiag_err_HTML . '</div>';
1721                //$content .= '</div>';
1722
1723                $json['content'] = self::compressHTML($content);
1724
1725                $this->sendHttpHeaders('Content-Type: application/json');
1726
1727                // when we fail here, it's pretty darn bad and nothing to it.
1728                // just push the error JSON and go.
1729                echo json_encode($jserr);
1730        }
1731
1732        /**
1733         * Process the 'destroy' event
1734         *
1735         * Delete the specified file or directory and return a JSON encoded status of success
1736         * or failure.
1737         *
1738         * Note that when images are deleted, so are their thumbnails.
1739         *
1740         * Expected parameters:
1741         *
1742         * $_POST['directory']     path relative to basedir a.k.a. options['directory'] root
1743         *
1744         * $_POST['file']          filename (including extension, of course) of the file to
1745         *                         be detailed.
1746         *
1747         * $_POST['filter']        optional mimetype filter string, amy be the part up to and
1748         *                         including the slash '/' or the full mimetype. Only files
1749         *                         matching this (set of) mimetypes will be listed.
1750         *                         Examples: 'image/' or 'application/zip'
1751         *
1752         * Errors will produce a JSON encoded error report, including at least two fields:
1753         *
1754         * status                  0 for error; nonzero for success
1755         *
1756         * error                   error message
1757         */
1758        protected function onDestroy()
1759        {
1760                $emsg = null;
1761                $file_arg = null;
1762                $legal_url = null;
1763                $jserr = array(
1764                                'status' => 1
1765                        );
1766
1767                try
1768                {
1769                        if (!$this->options['destroy'])
1770                                throw new FileManagerException('disabled:destroy');
1771
1772                        $v_ex_code = 'nofile';
1773
1774                        $file_arg = $this->getPOSTparam('file');
1775
1776                        $dir_arg = $this->getPOSTparam('directory');
1777                        $legal_url = $this->rel2abs_legal_url_path($dir_arg . '/');
1778
1779                        $mime_filter = $this->getPOSTparam('filter', $this->options['filter']);
1780                        $mime_filters = $this->getAllowedMimeTypes($mime_filter);
1781
1782                        $filename = null;
1783                        $file = null;
1784                        $mime = null;
1785                        $meta = null;
1786                        if (!empty($file_arg))
1787                        {
1788                                $filename = basename($file_arg);
1789                                $legal_url .= $filename;
1790                                // must transform here so alias/etc. expansions inside legal_url_path2file_path() get a chance:
1791                                $file = $this->legal_url_path2file_path($legal_url);
1792
1793                                if (file_exists($file))
1794                                {
1795                                        if (is_file($file))
1796                                        {
1797                                                $meta = $this->getFileInfo($file, $legal_url);
1798                                                $mime = $meta->getMimeType();
1799                                                if (!$this->IsAllowedMimeType($mime, $mime_filters))
1800                                                {
1801                                                        $v_ex_code = 'extension';
1802                                                }
1803                                                else
1804                                                {
1805                                                        $v_ex_code = null;
1806                                                }
1807                                        }
1808                                        else if (is_dir($file))
1809                                        {
1810                                                $mime = 'text/directory';
1811                                                $v_ex_code = null;
1812                                        }
1813                                }
1814                        }
1815
1816                        $fileinfo = array(
1817                                        'legal_url' => $legal_url,
1818                                        'file' => $file,
1819                                        'mime' => $mime,
1820                                        'meta_data' => $meta,
1821                                        'mime_filter' => $mime_filter,
1822                                        'mime_filters' => $mime_filters,
1823                                        'preliminary_json' => $jserr,
1824                                        'validation_failure' => $v_ex_code
1825                                );
1826
1827                        if (!empty($this->options['DestroyIsAuthorized_cb']) && function_exists($this->options['DestroyIsAuthorized_cb']) && !$this->options['DestroyIsAuthorized_cb']($this, 'destroy', $fileinfo))
1828                        {
1829                                $v_ex_code = $fileinfo['validation_failure'];
1830                                if (empty($v_ex_code)) $v_ex_code = 'authorized';
1831                        }
1832                        if (!empty($v_ex_code))
1833                                throw new FileManagerException($v_ex_code);
1834
1835                        $legal_url = $fileinfo['legal_url'];
1836                        $file = $fileinfo['file'];
1837                        $meta = $fileinfo['meta_data'];
1838                        $mime = $fileinfo['mime'];
1839                        $mime_filter = $fileinfo['mime_filter'];
1840                        $mime_filters = $fileinfo['mime_filters'];
1841                        $jserr = $fileinfo['preliminary_json'];
1842
1843                        if (!$this->unlink($legal_url, $mime_filters))
1844                        {
1845                                throw new FileManagerException('unlink_failed:' . $legal_url);
1846                        }
1847
1848                        $this->sendHttpHeaders('Content-Type: application/json');
1849
1850                        echo json_encode(array(
1851                                        'status' => 1,
1852                                        'content' => 'destroyed'
1853                                ));
1854                       
1855                        if (!empty($this->options['DestroyIsComplete_cb']) && function_exists($this->options['DestroyIsComplete_cb']))
1856                                $this->options['DestroyIsComplete_cb']($this, 'destroy', $fileinfo);   
1857                       
1858                        return;
1859                }
1860                catch(FileManagerException $e)
1861                {
1862                        $emsg = $e->getMessage();
1863                }
1864                catch(Exception $e)
1865                {
1866                        // catching other severe failures; since this can be anything and should only happen in the direst of circumstances, we don't bother translating
1867                        $emsg = $e->getMessage();
1868                }
1869
1870                $this->modify_json4exception($jserr, $emsg, 'file = ' . $file_arg . ', path = ' . $legal_url);
1871
1872                $this->sendHttpHeaders('Content-Type: application/json');
1873
1874                // when we fail here, it's pretty darn bad and nothing to it.
1875                // just push the error JSON and go.
1876                echo json_encode($jserr);
1877        }
1878
1879        /**
1880         * Process the 'create' event
1881         *
1882         * Create the specified subdirectory and give it the configured permissions
1883         * (options['chmod'], default 0777) and return a JSON encoded status of success
1884         * or failure.
1885         *
1886         * Expected parameters:
1887         *
1888         * $_POST['directory']     path relative to basedir a.k.a. options['directory'] root
1889         *
1890         * $_POST['file']          name of the subdirectory to be created
1891         *
1892         * Extra input parameters considered while producing the JSON encoded directory view.
1893         * These may not seem relevant for an empty directory, but these parameters are also
1894         * considered when providing the fallback directory view in case an error occurred
1895         * and then the listed directory (either the parent or the basedir itself) may very
1896         * likely not be empty!
1897         *
1898         * $_POST['filter']        optional mimetype filter string, amy be the part up to and
1899         *                         including the slash '/' or the full mimetype. Only files
1900         *                         matching this (set of) mimetypes will be listed.
1901         *                         Examples: 'image/' or 'application/zip'
1902         *
1903         * Errors will produce a JSON encoded error report, including at least two fields:
1904         *
1905         * status                  0 for error; nonzero for success
1906         *
1907         * error                   error message
1908         */
1909        protected function onCreate()
1910        {
1911                $emsg = null;
1912                $jserr = array(
1913                                'status' => 1
1914                        );
1915
1916                $mime_filter = $this->getPOSTparam('filter', $this->options['filter']);
1917
1918                $file_arg = null;
1919                $legal_url = null;
1920
1921                try
1922                {
1923                        if (!$this->options['create'])
1924                                throw new FileManagerException('disabled:create');
1925
1926                        $v_ex_code = 'nofile';
1927
1928                        $file_arg = $this->getPOSTparam('file');
1929
1930                        $dir_arg = $this->getPOSTparam('directory');
1931                        $legal_url = $this->rel2abs_legal_url_path($dir_arg . '/');
1932
1933                        // must transform here so alias/etc. expansions inside legal_url_path2file_path() get a chance:
1934                        $dir = $this->legal_url_path2file_path($legal_url);
1935
1936                        $filename = null;
1937                        $file = null;
1938                        $newdir = null;
1939                        if (!empty($file_arg))
1940                        {
1941                                $filename = basename($file_arg);
1942
1943                                if (!$this->IsHiddenNameAllowed($file_arg))
1944                                {
1945                                        $v_ex_code = 'authorized';
1946                                }
1947                                else
1948                                {
1949                                        if (is_dir($dir))
1950                                        {
1951                                                $file = $this->getUniqueName(array('filename' => $filename), $dir);  // a directory has no 'extension'!
1952                                                if ($file !== null)
1953                                                {
1954                                                        $newdir = $this->legal_url_path2file_path($legal_url . $file);
1955                                                        $v_ex_code = null;
1956                                                }
1957                                        }
1958                                }
1959                        }
1960
1961                        $fileinfo = array(
1962                                        'legal_url' => $legal_url,
1963                                        'dir' => $dir,
1964                                        'raw_name' => $filename,
1965                                        'uniq_name' => $file,
1966                                        'newdir' => $newdir,
1967                                        'chmod' => $this->options['chmod'],
1968                                        'preliminary_json' => $jserr,
1969                                        'validation_failure' => $v_ex_code
1970                                );
1971                        if (!empty($this->options['CreateIsAuthorized_cb']) && function_exists($this->options['CreateIsAuthorized_cb']) && !$this->options['CreateIsAuthorized_cb']($this, 'create', $fileinfo))
1972                        {
1973                                $v_ex_code = $fileinfo['validation_failure'];
1974                                if (empty($v_ex_code)) $v_ex_code = 'authorized';
1975                        }
1976                        if (!empty($v_ex_code))
1977                                throw new FileManagerException($v_ex_code);
1978
1979                        $legal_url = $fileinfo['legal_url'];
1980                        $dir = $fileinfo['dir'];
1981                        $filename = $fileinfo['raw_name'];
1982                        $file = $fileinfo['uniq_name'];
1983                        $newdir = $fileinfo['newdir'];
1984                        $jserr = $fileinfo['preliminary_json'];
1985
1986                        if (!@mkdir($newdir, $fileinfo['chmod'], true))
1987                        {
1988                                throw new FileManagerException('mkdir_failed:' . $this->legal2abs_url_path($legal_url) . $file);
1989                        }
1990
1991                        $this->sendHttpHeaders('Content-Type: application/json');
1992
1993                        // success, now show the new directory as a list view:
1994                        $rv = $this->_onView($legal_url . $file . '/', $jserr, $mime_filter);
1995
1996                        echo json_encode($rv);
1997                        return;
1998                }
1999                catch(FileManagerException $e)
2000                {
2001                        $emsg = $e->getMessage();
2002
2003                        $jserr['status'] = 0;
2004
2005                        // and fall back to showing the PARENT directory
2006                        try
2007                        {
2008                                $jserr = $this->_onView($legal_url, $jserr, $mime_filter);
2009                        }
2010                        catch (Exception $e)
2011                        {
2012                                // and fall back to showing the BASEDIR directory
2013                                try
2014                                {
2015                                        $legal_url = $this->options['directory'];
2016                                        $jserr = $this->_onView($legal_url, $jserr, $mime_filter);
2017                                }
2018                                catch (Exception $e)
2019                                {
2020                                        // when we fail here, it's pretty darn bad and nothing to it.
2021                                        // just push the error JSON and go.
2022                                }
2023                        }
2024                }
2025                catch(Exception $e)
2026                {
2027                        // catching other severe failures; since this can be anything and should only happen in the direst of circumstances, we don't bother translating
2028                        $emsg = $e->getMessage();
2029
2030                        $jserr['status'] = 0;
2031
2032                        // and fall back to showing the PARENT directory
2033                        try
2034                        {
2035                                $jserr = $this->_onView($legal_url, $jserr, $mime_filter);
2036                        }
2037                        catch (Exception $e)
2038                        {
2039                                // and fall back to showing the BASEDIR directory
2040                                try
2041                                {
2042                                        $legal_url = $this->options['directory'];
2043                                        $jserr = $this->_onView($legal_url, $jserr, $mime_filter);
2044                                }
2045                                catch (Exception $e)
2046                                {
2047                                        // when we fail here, it's pretty darn bad and nothing to it.
2048                                        // just push the error JSON and go.
2049                                }
2050                        }
2051                }
2052
2053                $this->modify_json4exception($jserr, $emsg, 'directory = ' . $file_arg . ', path = ' . $legal_url);
2054
2055                $this->sendHttpHeaders('Content-Type: application/json');
2056
2057                // when we fail here, it's pretty darn bad and nothing to it.
2058                // just push the error JSON and go.
2059                echo json_encode($jserr);
2060        }
2061
2062        /**
2063         * Process the 'download' event
2064         *
2065         * Send the file content of the specified file for download by the client.
2066         * Only files residing within the directory tree rooted by the
2067         * 'basedir' (options['directory']) will be allowed to be downloaded.
2068         *
2069         * Expected parameters:
2070         *
2071         * $_POST['file']         filepath of the file to be downloaded
2072         *
2073         * $_POST['filter']       optional mimetype filter string, amy be the part up to and
2074         *                        including the slash '/' or the full mimetype. Only files
2075         *                        matching this (set of) mimetypes will be listed.
2076         *                        Examples: 'image/' or 'application/zip'
2077         *
2078         * On errors a HTTP 403 error response will be sent instead.
2079         */
2080        protected function onDownload()
2081        {
2082                $emsg = null;
2083                $file_arg = null;
2084                $file = null;
2085                $jserr = array(
2086                                'status' => 1
2087                        );
2088               
2089                try
2090                {
2091                        if (!$this->options['download'])
2092                                throw new FileManagerException('disabled:download');
2093
2094                        $v_ex_code = 'nofile';
2095
2096                        $file_arg = $this->getPOSTparam('file');
2097
2098                        $mime_filter = $this->getPOSTparam('filter', $this->options['filter']);
2099                        $mime_filters = $this->getAllowedMimeTypes($mime_filter);
2100
2101                        $legal_url = null;
2102                        $file = null;
2103                        $mime = null;
2104                        $meta = null;
2105                        if (!empty($file_arg))
2106                        {
2107                                $legal_url = $this->rel2abs_legal_url_path($file_arg);
2108
2109                                // must transform here so alias/etc. expansions inside legal_url_path2file_path() get a chance:
2110                                $file = $this->legal_url_path2file_path($legal_url);
2111
2112                                if (is_readable($file))
2113                                {
2114                                        if (is_file($file))
2115                                        {
2116                                                $meta = $this->getFileInfo($file, $legal_url);
2117                                                $mime = $meta->getMimeType();
2118                                                if (!$this->IsAllowedMimeType($mime, $mime_filters))
2119                                                {
2120                                                        $v_ex_code = 'extension';
2121                                                }
2122                                                else
2123                                                {
2124                                                        $v_ex_code = null;
2125                                                }
2126                                        }
2127                                        else
2128                                        {
2129                                                $mime = 'text/directory';
2130                                        }
2131                                }
2132                        }
2133
2134                        $fileinfo = array(
2135                                        'legal_url' => $legal_url,
2136                                        'file' => $file,
2137                                        'mime' => $mime,
2138                                        'meta_data' => $meta,
2139                                        'mime_filter' => $mime_filter,
2140                                        'mime_filters' => $mime_filters,
2141                                        'validation_failure' => $v_ex_code
2142                                );
2143                        if (!empty($this->options['DownloadIsAuthorized_cb']) && function_exists($this->options['DownloadIsAuthorized_cb']) && !$this->options['DownloadIsAuthorized_cb']($this, 'download', $fileinfo))
2144                        {
2145                                $v_ex_code = $fileinfo['validation_failure'];
2146                                if (empty($v_ex_code)) $v_ex_code = 'authorized';
2147                        }
2148                        if (!empty($v_ex_code))
2149                                throw new FileManagerException($v_ex_code);
2150
2151                        $legal_url = $fileinfo['legal_url'];
2152                        $file = $fileinfo['file'];
2153                        $meta = $fileinfo['meta_data'];
2154                        $mime = $fileinfo['mime'];
2155                        $mime_filter = $fileinfo['mime_filter'];
2156                        $mime_filters = $fileinfo['mime_filters'];
2157
2158
2159                        if ($fd = fopen($file, 'rb'))
2160                        {
2161                                $fsize = filesize($file);
2162                                $fi = pathinfo($legal_url);
2163                               
2164                                // Based on the gist here: https://gist.github.com/854168
2165                                // Reference: http://codeutopia.net/blog/2009/03/06/sending-files-better-apache-mod_xsendfile-and-php/
2166                                // We should:
2167                                // 1. try to use Apache mod_xsendfile
2168                                // 2. Try to chunk the file into pieces
2169                                // 3. If the file is sufficiently small, send it directly
2170
2171                                $hdrs = array();
2172                                // see also: http://www.boutell.com/newfaq/creating/forcedownload.html
2173                                switch ($mime)
2174                                {
2175                                // add here more mime types for different file types and special handling by the client on download
2176                                case 'application/pdf':
2177                                        $hdrs[] = 'Content-Type: ' . $mime;
2178                                        break;
2179
2180                                default:
2181                                        $hdrs[] = 'Content-Type: application/octet-stream';
2182                                        break;
2183                                }
2184                               
2185                                $hdrs[] = 'Content-Disposition: attachment; filename="' . $fi['basename'] . '"'; // use 'attachment' to force a download
2186                               
2187                                // Content length isn't requied for mod_xsendfile (Apache handles this for us)
2188                                $modx = $this->options['enableXSendFile'] && function_exists('apache_get_modules') && in_array('mod_xsendfile', apache_get_modules());
2189                                if ($modx)
2190                                {
2191                                        $hdrs[] = 'X-Sendfile: '.$file;
2192                                }
2193                                else
2194                                {
2195                                        $hdrs[] = 'Content-length: ' . $fsize;
2196                                        $hdrs[] = 'Expires: 0';
2197                                        $hdrs[] = 'Cache-Control: must-revalidate, post-check=0, pre-check=0';
2198                                        $hdrs[] = '!Cache-Control: private'; // flag as FORCED APPEND; use this to open files directly
2199                                }
2200
2201                                $this->sendHttpHeaders($hdrs);
2202                               
2203                                if (!$modx)
2204                                {
2205                                        $chunksize = 4*1024; // 4KB blocks
2206                                        if ($fsize > $chunksize)
2207                                        {
2208                                                // Turn off compression which prevents files from being re-assembled properly (especially zip files)
2209                                                function_exists('apache_setenv') && @apache_setenv('no-gzip', 1);
2210                                                @ini_set('zlib.output_compression', 0);
2211                                               
2212                                                // Turn off any additional buffering by the server
2213                                                @ini_set('implicit_flush', 1);
2214                                               
2215                                                // Disable any timeouts
2216                                                @set_time_limit(0);
2217                                                while (!feof($fd))
2218                                                {
2219                                                        echo @fread($fd, $chunksize);
2220                                                        ob_flush();
2221                                                        flush();
2222                                                }
2223                                        }
2224                                        else
2225                                        {
2226                                                fpassthru($fd);
2227                                        }
2228                                }
2229
2230                                fclose($fd);
2231                               
2232                                if (!empty($this->options['DownloadIsComplete_cb']) && function_exists($this->options['DownloadIsComplete_cb']))
2233                                  $this->options['DownloadIsComplete_cb']($this, 'download', $fileinfo);
2234       
2235                                return;
2236                        }
2237                       
2238                        $emsg = 'read_error';
2239
2240                       
2241                }
2242                catch(FileManagerException $e)
2243                {
2244                        $emsg = $e->getMessage();
2245                }
2246                catch(Exception $e)
2247                {
2248                        // catching other severe failures; since this can be anything and should only happen in the direst of circumstances, we don't bother translating
2249                        $emsg = $e->getMessage();
2250                }
2251
2252                // we don't care whether it's a 404, a 403 or something else entirely: we feed 'em a 403 and that's final!
2253                send_response_status_header(403);
2254
2255                $this->modify_json4exception($jserr, $emsg, 'file = ' . $this->mkSafe4Display($file_arg . ', destination path = ' . $file));
2256               
2257                $this->sendHttpHeaders('Content-Type: text/plain');        // Safer for iframes: the 'application/json' mime type would cause FF3.X to pop up a save/view dialog when transmitting these error reports!
2258
2259                // when we fail here, it's pretty darn bad and nothing to it.
2260                // just push the error JSON and go.
2261                echo json_encode($jserr);
2262        }
2263
2264        /**
2265         * Process the 'upload' event
2266         *
2267         * Process and store the uploaded file in the designated location.
2268         * Images will be resized when possible and applicable. A thumbnail image will also
2269         * be preproduced when possible.
2270         * Return a JSON encoded status of success or failure.
2271         *
2272         * Expected parameters:
2273         *
2274         * $_POST['directory']    path relative to basedir a.k.a. options['directory'] root
2275         *
2276         * $_POST['resize']       nonzero value indicates any uploaded image should be resized to the configured
2277         *                        options['maxImageDimension'] width and height whenever possible
2278         *
2279         * $_POST['filter']       optional mimetype filter string, amy be the part up to and
2280         *                        including the slash '/' or the full mimetype. Only files
2281         *                        matching this (set of) mimetypes will be listed.
2282         *                        Examples: 'image/' or 'application/zip'
2283         *
2284         * $_FILES[]              the metadata for the uploaded file
2285         *
2286         * $_POST['reportContentType']
2287         *                        if you want a specific content type header set on our response, put it here.
2288         *                        This is needed for when we are posting an upload response to a hidden iframe, the
2289         *                        default application/json mimetype breaks down in that case at least for Firefox 3.X
2290         *                        as the browser will pop up a save/view dialog before JS can access the transmitted data.
2291         *
2292         * Errors will produce a JSON encoded error report, including at least two fields:
2293         *
2294         * status                 0 for error; nonzero for success
2295         *
2296         * error                  error message
2297         */
2298        protected function onUpload()
2299        {
2300                $emsg = null;
2301                $file_arg = null;
2302                $file = null;
2303                $legal_dir_url = null;
2304                $jserr = array(
2305                                'status' => 1
2306                        );
2307
2308                try
2309                {
2310                        if (!$this->options['upload'])
2311                                throw new FileManagerException('disabled:upload');
2312
2313                        // MAY upload zero length files!
2314                        if (!isset($_FILES) || empty($_FILES['Filedata']) || empty($_FILES['Filedata']['name']))
2315                                throw new FileManagerException('nofile');
2316
2317                        $v_ex_code = 'nofile';
2318
2319                        $file_size = (empty($_FILES['Filedata']['size']) ? 0 : $_FILES['Filedata']['size']);
2320
2321                        $file_arg = $_FILES['Filedata']['name'];
2322
2323                        $dir_arg = $this->getPOSTparam('directory');
2324                        $legal_dir_url = $this->rel2abs_legal_url_path($dir_arg . '/');
2325                        // must transform here so alias/etc. expansions inside legal_url_path2file_path() get a chance:
2326                        $dir = $this->legal_url_path2file_path($legal_dir_url);
2327
2328                        $mime_filter = $this->getPOSTparam('filter', $this->options['filter']);
2329                        $mime_filters = $this->getAllowedMimeTypes($mime_filter);
2330
2331                        $tmppath = $_FILES['Filedata']['tmp_name'];
2332
2333                        $resize_imgs = $this->getPOSTparam('resize', 0);
2334
2335                        $filename = null;
2336                        $fi = array('filename' => null, 'extension' => null);
2337                        $mime = null;
2338                        $meta = null;
2339                        if (!empty($file_arg))
2340                        {
2341                                if (!$this->IsHiddenNameAllowed($file_arg))
2342                                {
2343                                        $v_ex_code = 'fmt_not_allowed';
2344                                }
2345                                else
2346                                {
2347                                        $filename = $this->getUniqueName($file_arg, $dir);
2348                                        if ($filename !== null)
2349                                        {
2350                                                /*
2351                                                 * Security:
2352                                                 *
2353                                                 * Upload::move() processes the unfiltered version of $_FILES[]['name'], at least to get the extension,
2354                                                 * unless we ALWAYS override the filename and extension in the options array below. That's why we
2355                                                 * calculate the extension at all times here.
2356                                                 */
2357                                                if ($this->options['safe'])
2358                                                {
2359                                                        $fi = pathinfo($filename);
2360                                                        $fi['extension'] = $this->getSafeExtension(isset($fi['extension']) ? $fi['extension'] : '');
2361                                                        $filename = $fi['filename'] . ((isset($fi['extension']) && strlen($fi['extension']) > 0) ? '.' . $fi['extension'] : '');
2362                                                }
2363
2364                                                $legal_url = $legal_dir_url . $filename;
2365
2366                                                // UPLOAD delivers files in temporary storage with extensions NOT matching the mime type, so we don't
2367                                                // filter on extension; we just let getID3 go ahead and content-sniff the mime type.
2368                                                // Since getID3::analyze() is a quite costly operation, we like to do it only ONCE per file,
2369                                                // so we cache the last entries.
2370                                                $meta = $this->getFileInfo($tmppath, $legal_url);
2371                                                $mime = $meta->getMimeType();
2372                                                if (!$this->IsAllowedMimeType($mime, $mime_filters))
2373                                                {
2374                                                        $v_ex_code = 'extension';
2375                                                }
2376                                                else
2377                                                {
2378                                                        $v_ex_code = null;
2379                                                }
2380                                        }
2381                                }
2382                        }
2383
2384                        $fileinfo = array(
2385                                'legal_dir_url' => $legal_dir_url,
2386                                'dir' => $dir,
2387                                'raw_filename' => $file_arg,
2388                                'filename' => $filename,
2389                                'meta_data' => $meta,
2390                                'mime' => $mime,
2391                                'mime_filter' => $mime_filter,
2392                                'mime_filters' => $mime_filters,
2393                                'tmp_filepath' => $tmppath,
2394                                'size' => $file_size,
2395                                'maxsize' => $this->options['maxUploadSize'],
2396                                'overwrite' => false,
2397                                'resize' => $resize_imgs,
2398                                'chmod' => $this->options['chmod'] & 0666,   // security: never make those files 'executable'!
2399                                'preliminary_json' => $jserr,
2400                                'validation_failure' => $v_ex_code
2401                        );
2402                        if (!empty($this->options['UploadIsAuthorized_cb']) && function_exists($this->options['UploadIsAuthorized_cb']) && !$this->options['UploadIsAuthorized_cb']($this, 'upload', $fileinfo))
2403                        {
2404                                $v_ex_code = $fileinfo['validation_failure'];
2405                                if (empty($v_ex_code)) $v_ex_code = 'authorized';
2406                        }
2407                        if (!empty($v_ex_code))
2408                                throw new FileManagerException($v_ex_code);
2409
2410                        $legal_dir_url = $fileinfo['legal_dir_url'];
2411                        $dir = $fileinfo['dir'];
2412                        $file_arg = $fileinfo['raw_filename'];
2413                        $filename = $fileinfo['filename'];
2414                        $meta = $fileinfo['meta_data'];
2415                        $mime = $fileinfo['mime'];
2416                        $mime_filter = $fileinfo['mime_filter'];
2417                        $mime_filters = $fileinfo['mime_filters'];
2418                        //$tmppath = $fileinfo['tmp_filepath'];
2419                        $resize_imgs = $fileinfo['resize'];
2420                        $jserr = $fileinfo['preliminary_json'];
2421
2422                        if ($fileinfo['maxsize'] && $fileinfo['size'] > $fileinfo['maxsize'])
2423                                throw new FileManagerException('size');
2424
2425                        //if (!isset($fileinfo['extension']))
2426                        //  throw new FileManagerException('extension');
2427
2428                        // must transform here so alias/etc. expansions inside legal_url_path2file_path() get a chance:
2429                        $legal_url = $legal_dir_url . $filename;
2430                        $file = $this->legal_url_path2file_path($legal_url);
2431
2432                        if (!$fileinfo['overwrite'] && file_exists($file))
2433                                throw new FileManagerException('exists');
2434
2435                        if (!@move_uploaded_file($_FILES['Filedata']['tmp_name'], $file))
2436                        {
2437                                $emsg = 'path';
2438                                switch ($_FILES['Filedata']['error'])
2439                                {
2440                                case 1:
2441                                case 2:
2442                                        $emsg = 'size';
2443                                        break;
2444
2445                                case 3:
2446                                        $emsg = 'partial';
2447                                        break;
2448
2449                                default:
2450                                        $dir = $this->legal_url_path2file_path($legal_dir_url);
2451                                        if (!is_dir($dir))
2452                                        {
2453                                                $emsg = 'path';
2454                                        }
2455                                        else if (!is_writable($dir))
2456                                        {
2457                                                $emsg = 'path_not_writable';
2458                                        }
2459                                        else
2460                                        {
2461                                                $emsg = 'filename_maybe_too_large';
2462                                        }
2463
2464                                        if (!empty($_FILES['Filedata']['error']))
2465                                        {
2466                                                $emsg .= ': error code = ' . strtolower($_FILES['Filedata']['error']) . ', ' . $emsg_add;
2467                                        }
2468                                        break;
2469                                }
2470                                throw new FileManagerException($emsg);
2471                        }
2472
2473                        @chmod($file, $fileinfo['chmod']);
2474
2475                        /*
2476                         * NOTE: you /can/ (and should be able to, IMHO) upload 'overly large' image files to your site, but the resizing process step
2477                         *       happening here will fail; we have memory usage estimators in place to make the fatal crash a non-silent one, i,e, one
2478                         *       where we still have a very high probability of NOT fatally crashing the PHP iunterpreter but catching a suitable exception
2479                         *       instead.
2480                         *       Having uploaded such huge images, a developer/somebody can always go in later and up the memory limit if the site admins
2481                         *       feel it is deserved. Until then, no thumbnails of such images (though you /should/ be able to milkbox-view the real thing!)
2482                         */
2483                        $thumb250   = false;
2484                        $thumb250_e = false;
2485                        $thumb48    = false;
2486                        $thumb48_e  = false;
2487                        if (FileManagerUtility::startsWith($mime, 'image/'))
2488                        {
2489                                if (!empty($resize_imgs))
2490                                {
2491                                        $img = new Image($file);
2492                                        $size = $img->getSize();
2493                                        // Image::resize() takes care to maintain the proper aspect ratio, so this is easy
2494                                        // (default quality is 100% for JPEG so we get the cleanest resized images here)
2495                                        $img->resize($this->options['maxImageDimension']['width'], $this->options['maxImageDimension']['height'])->save();
2496                                        unset($img);
2497                                       
2498                                        // source image has changed: nuke the cached metadata and then refetch the metadata = forced refetch
2499                                        $meta = $this->getFileInfo($file, $legal_url, true);
2500                                }
2501                        }
2502
2503                        /*
2504                         * 'abuse' the info extraction process to generate the thumbnails. Besides, doing it this way will also prime the metadata cache for this item,
2505                         * so we'll have a very fast performance viewing this file's details and thumbnails both from this point forward!
2506                         */
2507                        $jsbogus = array('status' => 1);
2508                        $jsbogus = $this->extractDetailInfo($jsbogus, $legal_url, $meta, $mime_filter, $mime_filters, array('direct'));
2509
2510                        $this->sendHttpHeaders('Content-Type: ' . $this->getPOSTparam('reportContentType', 'application/json'));
2511
2512                        echo json_encode(array(
2513                                        'status' => 1,
2514                                        'name' => basename($file)
2515                                ));
2516                       
2517                        if (!empty($this->options['UploadIsComplete_cb']) && function_exists($this->options['UploadIsComplete_cb']))
2518                                $this->options['UploadIsComplete_cb']($this, 'upload', $fileinfo);
2519                       
2520                        return;
2521                }
2522                catch(FileManagerException $e)
2523                {
2524                        $emsg = $e->getMessage();
2525                }
2526                catch(Exception $e)
2527                {
2528                        // catching other severe failures; since this can be anything and should only happen in the direst of circumstances, we don't bother translating
2529                        $emsg = $e->getMessage();
2530                }
2531
2532                $this->modify_json4exception($jserr, $emsg, 'file = ' . $this->mkSafe4Display($file_arg . ', destination path = ' . $file . ', target directory (URI path) = ' . $legal_dir_url));
2533
2534                $this->sendHttpHeaders('Content-Type: ' . $this->getPOSTparam('reportContentType', 'application/json'));
2535
2536                // when we fail here, it's pretty darn bad and nothing to it.
2537                // just push the error JSON and go.
2538                echo json_encode(array_merge($jserr, $_FILES));
2539        }
2540
2541        /**
2542         * Process the 'move' event (with is used by both move/copy and rename client side actions)
2543         *
2544         * Copy or move/rename a given file or directory and return a JSON encoded status of success
2545         * or failure.
2546         *
2547         * Expected parameters:
2548         *
2549         *   $_POST['copy']          nonzero value means copy, zero or nil for move/rename
2550         *
2551         * Source filespec:
2552         *
2553         *   $_POST['directory']     path relative to basedir a.k.a. options['directory'] root
2554         *
2555         *   $_POST['file']          original name of the file/subdirectory to be renamed/copied
2556         *
2557         * Destination filespec:
2558         *
2559         *   $_POST['newDirectory']  path relative to basedir a.k.a. options['directory'] root;
2560         *                           target directory where the file must be moved / copied
2561         *
2562         *   $_POST['name']          target name of the file/subdirectory to be renamed
2563         *
2564         * Errors will produce a JSON encoded error report, including at least two fields:
2565         *
2566         * status                    0 for error; nonzero for success
2567         *
2568         * error                     error message
2569         */
2570        protected function onMove()
2571        {
2572                $emsg = null;
2573                $file_arg = null;
2574                $legal_url = null;
2575                $newpath = null;
2576                $jserr = array(
2577                                'status' => 1
2578                        );
2579
2580                try
2581                {
2582                        if (!$this->options['move'])
2583                                throw new FileManagerException('disabled:rn_mv_cp');
2584
2585                        $v_ex_code = 'nofile';
2586
2587                        $file_arg = $this->getPOSTparam('file');
2588
2589                        $dir_arg = $this->getPOSTparam('directory');
2590                        $legal_url = $this->rel2abs_legal_url_path($dir_arg . '/');
2591
2592                        // must transform here so alias/etc. expansions inside legal_url_path2file_path() get a chance:
2593                        $dir = $this->legal_url_path2file_path($legal_url);
2594
2595                        $newdir_arg = $this->getPOSTparam('newDirectory');
2596                        $newname_arg = $this->getPOSTparam('name');
2597                        $rename = (empty($newdir_arg) && !empty($newname_arg));
2598
2599                        $is_copy = !!$this->getPOSTparam('copy');
2600
2601                        $filename = null;
2602                        $path = null;
2603                        $fn = null;
2604                        $legal_newurl = null;
2605                        $newdir = null;
2606                        $newname = null;
2607                        $newpath = null;
2608                        $is_dir = false;
2609                        if (!$this->IsHiddenPathAllowed($newdir_arg) || !$this->IsHiddenNameAllowed($newname_arg))
2610                        {
2611                                $v_ex_code = 'authorized';
2612                        }
2613                        else
2614                        {
2615                                if (!empty($file_arg))
2616                                {
2617                                        $filename = basename($file_arg);
2618                                        $path = $this->legal_url_path2file_path($legal_url . $filename);
2619
2620                                        if (file_exists($path))
2621                                        {
2622                                                $is_dir = is_dir($path);
2623
2624                                                // note: we do not support copying entire directories, though directory rename/move is okay
2625                                                if ($is_copy && $is_dir)
2626                                                {
2627                                                        $v_ex_code = 'disabled:rn_mv_cp';
2628                                                }
2629                                                else if ($rename)
2630                                                {
2631                                                        $fn = 'rename';
2632                                                        $legal_newurl = $legal_url;
2633                                                        $newdir = $dir;
2634
2635                                                        $newname = basename($newname_arg);
2636                                                        if ($is_dir)
2637                                                                $newname = $this->getUniqueName(array('filename' => $newname), $newdir);  // a directory has no 'extension'
2638                                                        else
2639                                                                $newname = $this->getUniqueName($newname, $newdir);
2640
2641                                                        if ($newname === null)
2642                                                        {
2643                                                                $v_ex_code = 'nonewfile';
2644                                                        }
2645                                                        else
2646                                                        {
2647                                                                // when the new name seems to have a different extension, make sure the extension doesn't change after all:
2648                                                                // Note: - if it's only 'case' we're changing here, then exchange the extension instead of appending it.
2649                                                                //       - directories do not have extensions
2650                                                                $extOld = pathinfo($filename, PATHINFO_EXTENSION);
2651                                                                $extNew = pathinfo($newname, PATHINFO_EXTENSION);
2652                                                                if ((!$this->options['allowExtChange'] || (!$is_dir && empty($extNew))) && !empty($extOld) && strtolower($extOld) != strtolower($extNew))
2653                                                                {
2654                                                                        $newname .= '.' . $extOld;
2655                                                                }
2656                                                                $v_ex_code = null;
2657                                                        }
2658                                                }
2659                                                else
2660                                                {
2661                                                        $fn = ($is_copy ? 'copy' : 'rename' /* 'move' */);
2662                                                        $legal_newurl = $this->rel2abs_legal_url_path($newdir_arg . '/');
2663                                                        $newdir = $this->legal_url_path2file_path($legal_newurl);
2664
2665                                                        if ($is_dir)
2666                                                                $newname = $this->getUniqueName(array('filename' => $filename), $newdir);  // a directory has no 'extension'
2667                                                        else
2668                                                                $newname = $this->getUniqueName($filename, $newdir);
2669
2670                                                        if ($newname === null)
2671                                                                $v_ex_code = 'nonewfile';
2672                                                        else
2673                                                                $v_ex_code = null;
2674                                                }
2675
2676                                                if (empty($v_ex_code))
2677                                                {
2678                                                        $newpath = $this->legal_url_path2file_path($legal_newurl . $newname);
2679                                                }
2680                                        }
2681                                }
2682                        }
2683
2684                        $fileinfo = array(
2685                                        'legal_url' => $legal_url,
2686                                        'dir' => $dir,
2687                                        'path' => $path,
2688                                        'name' => $filename,
2689                                        'legal_newurl' => $legal_newurl,
2690                                        'newdir' => $newdir,
2691                                        'newpath' => $newpath,
2692                                        'newname' => $newname,
2693                                        'rename' => $rename,
2694                                        'is_dir' => $is_dir,
2695                                        'function' => $fn,
2696                                        'preliminary_json' => $jserr,
2697                                        'validation_failure' => $v_ex_code
2698                                );
2699
2700                        if (!empty($this->options['MoveIsAuthorized_cb']) && function_exists($this->options['MoveIsAuthorized_cb']) && !$this->options['MoveIsAuthorized_cb']($this, 'move', $fileinfo))
2701                        {
2702                                $v_ex_code = $fileinfo['validation_failure'];
2703                                if (empty($v_ex_code)) $v_ex_code = 'authorized';
2704                        }
2705                        if (!empty($v_ex_code))
2706                                throw new FileManagerException($v_ex_code);
2707
2708                        $legal_url = $fileinfo['legal_url'];
2709                        $dir = $fileinfo['dir'];
2710                        $path = $fileinfo['path'];
2711                        $filename = $fileinfo['name'];
2712                        $legal_newurl = $fileinfo['legal_newurl'];
2713                        $newdir = $fileinfo['newdir'];
2714                        $newpath = $fileinfo['newpath'];
2715                        $newname = $fileinfo['newname'];
2716                        $rename = $fileinfo['rename'];
2717                        $is_dir = $fileinfo['is_dir'];
2718                        $fn = $fileinfo['function'];
2719                        $jserr = $fileinfo['preliminary_json'];
2720
2721                        if ($rename)
2722                        {
2723                                // try to remove the thumbnail & other cache entries related to the original file; don't mind if it doesn't exist
2724                                $flurl = $legal_url . $filename;
2725                                $meta = &$this->getid3_cache->pick($flurl, $this, false);
2726                                assert($meta != null);
2727                                if (!$meta->delete(true))
2728                                {
2729                                        throw new FileManagerException('delete_cache_entries_failed');
2730                                }
2731                                unset($meta);
2732                        }
2733
2734                        if (!function_exists($fn))
2735                                throw new FileManagerException((empty($fn) ? 'rename' : $fn) . '_failed');
2736                        if (!@$fn($path, $newpath))
2737                                throw new FileManagerException($fn . '_failed');
2738
2739                        $this->sendHttpHeaders('Content-Type: application/json');
2740
2741                        // jserr['status'] == 1
2742                        $jserr['name'] = $newname;
2743
2744                        echo json_encode($jserr);
2745                        return;
2746                }
2747                catch(FileManagerException $e)
2748                {
2749                        $emsg = $e->getMessage();
2750                }
2751                catch(Exception $e)
2752                {
2753                        // catching other severe failures; since this can be anything and should only happen in the direst of circumstances, we don't bother translating
2754                        $emsg = $e->getMessage();
2755                }
2756
2757                $this->modify_json4exception($jserr, $emsg, 'file = ' . $file_arg . ', path = ' . $legal_url . ', destination path = ' . $newpath);
2758
2759                $this->sendHttpHeaders('Content-Type: application/json');
2760
2761                // when we fail here, it's pretty darn bad and nothing to it.
2762                // just push the error JSON and go.
2763                echo json_encode($jserr);
2764        }
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775        /**
2776         * Send the listed headers when possible; the input parameter is an array of header strings or a single header string.
2777         *
2778         * NOTE: when a header string starts with the '!' character, it means that header is required
2779         * to be appended to the header output set and not overwrite any existing equal header.
2780         */
2781        public function sendHttpHeaders($headers)
2782        {
2783                if (!headers_sent())
2784                {
2785                        $headers = array_merge(array(
2786                                'Expires: Fri, 01 Jan 1990 00:00:00 GMT',
2787                                'Cache-Control: no-cache, no-store, max-age=0, must-revalidate'
2788                        ), (is_array($headers) ? $headers : array($headers)));
2789
2790                        foreach($headers as $h)
2791                        {
2792                                $append_flag = ($h[0] == '!');
2793                                $h = ltrim($h, '!');
2794                                header($h, $append_flag);
2795                        }
2796                }
2797        }
2798
2799
2800
2801
2802        // derived from   http://www.php.net/manual/en/function.filesize.php#100097
2803        public function format_bytes($bytes)
2804        {
2805                if ($bytes < 1024)
2806                        return $bytes . ' Bytes';
2807                elseif ($bytes < 1048576)
2808                        return round($bytes / 1024, 2) . ' KB (' . $bytes . ' Bytes)';
2809                elseif ($bytes < 1073741824)
2810                        return round($bytes / 1048576, 2) . ' MB (' . $bytes . ' Bytes)';
2811                else
2812                        return round($bytes / 1073741824, 2) . ' GB (' . $bytes . ' Bytes)';
2813        }
2814
2815        /**
2816         * Produce a HTML snippet detailing the given file in the JSON 'content' element; place additional info
2817         * in the JSON elements 'thumbnail', 'thumb48', 'thumb250', 'width', 'height', ...
2818         *
2819         * Return an augmented JSON array.
2820         *
2821         * Throw an exception on error.
2822         */
2823        public function extractDetailInfo($json_in, $legal_url, &$meta, $mime_filter, $mime_filters, $mode)
2824        {
2825                $auto_thumb_gen_mode = !in_array('direct', $mode, true);
2826                $metaHTML_mode = in_array('metaHTML', $mode, true);
2827                $metaJSON_mode = in_array('metaJSON', $mode, true);
2828
2829                $url = $this->legal2abs_url_path($legal_url);
2830                $filename = basename($url);
2831
2832                // must transform here so alias/etc. expansions inside url_path2file_path() get a chance:
2833                $file = $this->url_path2file_path($url);
2834
2835                $isdir = !is_file($file);
2836                $bad_ext = false;
2837                $mime = null;
2838                // only perform the (costly) getID3 scan when it hasn't been done before, i.e. can we re-use previously obtained data or not?
2839                if (!is_object($meta))
2840                {
2841                        $meta = $this->getFileInfo($file, $legal_url);
2842                }
2843                if (!$isdir)
2844                {
2845                        $mime = $meta->getMimeType();
2846
2847                        $mime2 = $this->getMimeFromExt($file);
2848                        $meta->store('mime_type from file extension', $mime2);
2849
2850                        $bad_ext = ($mime2 != $mime);
2851                        if ($bad_ext)
2852                        {
2853                                $iconspec = 'is.' + $this->getExtFromMime($mime);
2854                        }
2855                        else
2856                        {
2857                                $iconspec = $filename;
2858                        }
2859
2860                        if (!$this->IsAllowedMimeType($mime, $mime_filters))
2861                                throw new FileManagerException('extension');
2862                }
2863                else if (is_dir($file))
2864                {
2865                        $mime = $meta->getMimeType();
2866                        // $mime = 'text/directory';
2867                        $iconspec = 'is.directory';
2868                }
2869                else
2870                {
2871                        // simply do NOT list anything that we cannot cope with.
2872                        // That includes clearly inaccessible files (and paths) with non-ASCII characters:
2873                        // PHP5 and below are a real mess when it comes to handling Unicode filesystems
2874                        // (see the php.net site too: readdir / glob / etc. user comments and the official
2875                        // notice that PHP will support filesystem UTF-8/Unicode only when PHP6 is released.
2876                        //
2877                        // Big, fat bummer!
2878                        throw new FileManagerException('nofile');
2879                }
2880
2881                // as all the work below is quite costly, we check whether the already loaded cache entry got our number:
2882                // several chunks of work below may have been cached and when they have been, use the cached data.
2883
2884                // it's an internal error when this entry do not exist in the cache store by now!
2885                $fi = $meta->fetch('analysis');
2886                //assert(!empty($fi));
2887
2888                $icon48 = $this->getIcon($iconspec, false);
2889                $icon = $this->getIcon($iconspec, true);
2890
2891                $thumb250 = $meta->fetch('thumb250_direct');
2892                $thumb48 = $meta->fetch('thumb48_direct');
2893                $thumb250_e = false;
2894                $thumb48_e  = false;
2895
2896                $tstamp_str = date($this->options['dateFormat'], @filemtime($file));
2897                $fsize = @filesize($file);
2898                $fsize_str = empty($fsize) ? '-' : $this->format_bytes($fsize); // convert to T/G/M/K-bytes:
2899   
2900                $json = array_merge(array(
2901                                //'status' => 1,
2902                                //'mimetype' => $mime,
2903                                'content' => self::compressHTML('<div class="margin">
2904                                        ${nopreview}
2905                                </div>')
2906                        ),
2907                        array(
2908                                'path' => $legal_url,
2909                                'url'  => $url,
2910                                'name' => $filename,
2911                                'date' => $tstamp_str,
2912                                'mime' => $mime,
2913                                'size' => $fsize,
2914                                'modified' => @filemtime($file),
2915                                'size_str' => $fsize_str
2916                        ));
2917
2918                $content = '<dl>
2919                                                <dt>${modified}</dt>
2920                                                <dd class="filemanager-modified">' . $tstamp_str . '</dd>
2921                                                <dt>${type}</dt>
2922                                                <dd class="filemanager-type">' . $mime . '</dd>
2923                                                <dt>${size}</dt>
2924                                                <dd class="filemanager-size">' . $fsize_str . '</dd>';
2925                $content_dl_term = false;
2926
2927                $preview_HTML = null;
2928                $postdiag_err_HTML = '';
2929                $postdiag_dump_HTML = '';
2930                $thumbnails_done_or_deferred = false;   // TRUE: mark our thumbnail work as 'done'; any NULL thumbnails represent deferred generation entries!
2931                $check_for_embedded_img = false;
2932
2933                $mime_els = explode('/', $mime);
2934                for(;;) // bogus loop; only meant to assist the [mime remapping] state machine in here
2935                {
2936                        switch ($mime_els[0])
2937                        {
2938                        case 'image':
2939                                /*
2940                                 * thumbnail_gen_mode === 'auto':
2941                                 *
2942                                 * offload the thumbnailing process to another event ('event=thumbnail') to be fired by the client
2943                                 * when it's time to render the thumbnail:
2944                                 * WE simply assume the thumbnail will be there, and when it doesn't, that's
2945                                 * for the event=thumbnail handler to worry about (creating the thumbnail on demand or serving
2946                                 * a generic icon image instead). Meanwhile, we are able to speed up the response process here quite
2947                                 * a bit (rendering thumbnails from very large images can take a lot of time!)
2948                                 *
2949                                 * To further improve matters, we first generate the 250px thumbnail and then generate the 48px
2950                                 * thumbnail from that one (if it doesn't already exist). That saves us one more time processing
2951                                 * the (possibly huge) original image; downscaling the 250px file is quite fast, relatively speaking.
2952                                 *
2953                                 * That bit of code ASSUMES that the thumbnail will be generated from the file argument, while
2954                                 * the url argument is used to determine the thumbnail name/path.
2955                                 */
2956                                $emsg = null;
2957                                try
2958                                {
2959                                        if (empty($thumb250))
2960                                        {
2961                                                $thumb250 = $this->getThumb($meta, $file, $this->options['thumbBigSize'], $this->options['thumbBigSize'], $auto_thumb_gen_mode);
2962                                        }
2963                                        if (!empty($thumb250))
2964                                        {
2965                                                $thumb250_e = FileManagerUtility::rawurlencode_path($thumb250);
2966                                        }
2967                                        if (empty($thumb48))
2968                                        {
2969                                                $thumb48 = $this->getThumb($meta, (!empty($thumb250) ? $this->url_path2file_path($thumb250) : $file), $this->options['thumbSmallSize'], $this->options['thumbSmallSize'], $auto_thumb_gen_mode);
2970                                        }
2971                                        if (!empty($thumb48))
2972                                        {
2973                                                $thumb48_e = FileManagerUtility::rawurlencode_path($thumb48);
2974                                        }
2975
2976                                        if (empty($thumb48) || empty($thumb250))
2977                                        {
2978                                                /*
2979                                                 * do NOT generate the thumbnail itself yet (it takes too much time!) but do check whether it CAN be generated
2980                                                 * at all: THAT is a (relatively speaking) fast operation!
2981                                                 */
2982                                                $imginfo = Image::checkFileForProcessing($file);
2983                                        }
2984                                        $thumbnails_done_or_deferred = true;
2985                                }
2986                                catch (Exception $e)
2987                                {
2988                                        $emsg = $e->getMessage();
2989                                        $icon48 = $this->getIconForError($emsg, $legal_url, false);
2990                                        $icon = $this->getIconForError($emsg, $legal_url, true);
2991                                        // even cache the fail: that means next time around we don't suffer the failure but immediately serve the error icon instead.
2992                                }
2993
2994                                $width = round($this->getID3infoItem($fi, 0, 'video', 'resolution_x'));
2995                                $height = round($this->getID3infoItem($fi, 0, 'video', 'resolution_y'));
2996                                $json['width'] = $width;
2997                                $json['height'] = $height;
2998
2999                                $content .= '
3000                                                <dt>${width}</dt><dd>' . $width . 'px</dd>
3001                                                <dt>${height}</dt><dd>' . $height . 'px</dd>
3002                                        </dl>';
3003                                $content_dl_term = true;
3004
3005                                $sw_make = $this->mkSafeUTF8($this->getID3infoItem($fi, null, 'jpg', 'exif', 'IFD0', 'Software'));
3006                                $time_make = $this->mkSafeUTF8($this->getID3infoItem($fi, null, 'jpg', 'exif', 'IFD0', 'DateTime'));
3007
3008                                if (!empty($sw_make) || !empty($time_make))
3009                                {
3010                                        $content .= '<p>Made with ' . (empty($sw_make) ? '???' : $sw_make) . ' @ ' . (empty($time_make) ? '???' : $time_make) . '</p>';
3011                                }
3012
3013                                // are we delaying the thumbnail generation? When yes, then we need to infer the thumbnail dimensions *anyway*!
3014                                if (empty($thumb48) && $thumbnails_done_or_deferred)
3015                                {
3016                                        $dims = $this->predictThumbDimensions($width, $height, $this->options['thumbSmallSize'], $this->options['thumbSmallSize']);
3017
3018                                        $json['thumb48_width'] = $dims['width'];
3019                                        $json['thumb48_height'] = $dims['height'];
3020                                }
3021                                if (empty($thumb250))
3022                                {
3023                                        if ($thumbnails_done_or_deferred)
3024                                        {
3025                                                // to show the loader.gif in the preview <img> tag, we MUST set a width+height there, so we guestimate the thumbnail250 size as accurately as possible
3026                                                //
3027                                                // derive size from original:
3028                                                $dims = $this->predictThumbDimensions($width, $height, $this->options['thumbBigSize'], $this->options['thumbBigSize']);
3029
3030                                                $preview_HTML = '<a href="' . FileManagerUtility::rawurlencode_path($url) . '" data-milkbox="single" title="' . htmlentities($filename, ENT_QUOTES, 'UTF-8') . '">
3031                                                                           <img src="' . $this->options['assetBasePath'] . 'Images/transparent.gif" class="preview" alt="preview" style="width: ' . $dims['width'] . 'px; height: ' . $dims['height'] . 'px;" />
3032                                                                         </a>';
3033
3034                                                $json['thumb250_width'] = $dims['width'];
3035                                                $json['thumb250_height'] = $dims['height'];
3036                                        }
3037                                        else
3038                                        {
3039                                                // when we get here, a failure occurred before, so we only will have the icons. So we use those:
3040                                                $preview_HTML = '<a href="' . FileManagerUtility::rawurlencode_path($url) . '" data-milkbox="single" title="' . htmlentities($filename, ENT_QUOTES, 'UTF-8') . '">
3041                                                                           <img src="' . FileManagerUtility::rawurlencode_path($icon48) . '" class="preview" alt="preview" />
3042                                                                         </a>';
3043                                        }
3044                                }
3045                                // else: defer the $preview_HTML production until we're at the end of this and have fetched the actual thumbnail dimensions
3046
3047                                if (!empty($emsg))
3048                                {
3049                                        // use the abilities of modify_json4exception() to munge/format the exception message:
3050                                        $jsa = array('error' => '');
3051                                        $this->modify_json4exception($jsa, $emsg, 'path = ' . $url);
3052                                        $postdiag_err_HTML .= "\n" . '<p class="err_info">' . $jsa['error'] . '</p>';
3053
3054                                        if (strpos($emsg, 'img_will_not_fit') !== false)
3055                                        {
3056                                                $earr = explode(':', $emsg, 2);
3057                                                $postdiag_err_HTML .= "\n" . '<p class="tech_info">Estimated minimum memory requirements to create thumbnails for this image: ' . $earr[1] . '</p>';
3058                                        }
3059                                }
3060                                break;
3061
3062                        case 'text':
3063                                switch ($mime_els[1])
3064                                {
3065                                case 'directory':
3066                                        $preview_HTML = '';
3067                                        break;
3068
3069                                default:
3070                                        // text preview:
3071                                        $filecontent = @file_get_contents($file, false, null, 0);
3072                                        if ($filecontent === false)
3073                                                throw new FileManagerException('nofile');
3074
3075                                        if (!FileManagerUtility::isBinary($filecontent))
3076                                        {
3077                                                $preview_HTML = '<pre>' . str_replace(array('$', "\t"), array('&#36;', '&nbsp;&nbsp;'), htmlentities($filecontent, ENT_NOQUOTES, 'UTF-8')) . '</pre>';
3078                                        }
3079                                        else
3080                                        {
3081                                                // else: fall back to 'no preview available' (if getID3 didn't deliver instead...)
3082                                                $mime_els[0] = 'unknown'; // remap!
3083                                                continue 3;
3084                                        }
3085                                        break;
3086                                }
3087                                break;
3088
3089                        case 'application':
3090                                switch ($mime_els[1])
3091                                {
3092                                case 'x-javascript':
3093                                        $mime_els[0] = 'text'; // remap!
3094                                        continue 3;
3095
3096                                case 'zip':
3097                                        $out = array(array(), array());
3098                                        $info = $this->getID3infoItem($fi, null, 'zip', 'files');
3099                                        if (is_array($info))
3100                                        {
3101                                                foreach ($info as $name => $size)
3102                                                {
3103                                                        $name = $this->mkSafeUTF8($name);
3104                                                        $isdir = is_array($size);
3105                                                        $out[$isdir ? 0 : 1][$name] = '<li><a><img src="' . FileManagerUtility::rawurlencode_path($this->getIcon($name, true)) . '" alt="" /> ' . $name . '</a></li>';
3106                                                }
3107                                                natcasesort($out[0]);
3108                                                natcasesort($out[1]);
3109                                                $preview_HTML = '<ul>' . implode(array_merge($out[0], $out[1])) . '</ul>';
3110                                        }
3111                                        break;
3112
3113                                case 'x-shockwave-flash':
3114                                        $check_for_embedded_img = true;
3115
3116                                        $info = $this->getID3infoItem($fi, null, 'swf', 'header');
3117                                        if (is_array($info))
3118                                        {
3119                                                $width = round($this->getID3infoItem($fi, 0, 'swf', 'header', 'frame_width') / 10);
3120                                                $height = round($this->getID3infoItem($fi, 0, 'swf', 'header', 'frame_height') / 10);
3121                                                $json['width'] = $width;
3122                                                $json['height'] = $height;
3123
3124                                                $content .= '
3125                                                                <dt>${width}</dt><dd>' . $width . 'px</dd>
3126                                                                <dt>${height}</dt><dd>' . $height . 'px</dd>
3127                                                                <dt>${length}</dt><dd>' . round($this->getID3infoItem($fi, 0, 'swf', 'header', 'length') / $this->getID3infoItem($fi, 25, 'swf', 'header', 'frame_count')) . 's</dd>
3128                                                        </dl>';
3129                                                $content_dl_term = true;
3130                                        }
3131                                        break;
3132
3133                                default:
3134                                        // else: fall back to 'no preview available' (if getID3 didn't deliver instead...)
3135                                        $mime_els[0] = 'unknown'; // remap!
3136                                        continue 3;
3137                                }
3138                                break;
3139
3140                        case 'audio':
3141                                $check_for_embedded_img = true;
3142
3143                                $title = $this->mkSafeUTF8($this->getID3infoItem($fi, $this->getID3infoItem($fi, '???', 'tags', 'id3v1', 'title', 0), 'tags', 'id3v2', 'title', 0));
3144                                $artist = $this->mkSafeUTF8($this->getID3infoItem($fi, $this->getID3infoItem($fi, '???', 'tags', 'id3v1', 'artist', 0), 'tags', 'id3v2', 'artist', 0));
3145                                $album = $this->mkSafeUTF8($this->getID3infoItem($fi, $this->getID3infoItem($fi, '???', 'tags', 'id3v1', 'album', 0), 'tags', 'id3v2', 'album', 0));
3146                                $length =  $this->mkSafeUTF8($this->getID3infoItem($fi, '???', 'playtime_string'));
3147                                $bitrate = round($this->getID3infoItem($fi, 0, 'bitrate') / 1000);
3148                               
3149        $json = array_merge($json, array('title' => $title, 'artist' => $artist, 'album' => $album, 'length' => $length, 'bitrate' => $bitrate));
3150       
3151                                $content .= '
3152                                                <dt>${title}</dt><dd>' . $title . '</dd>
3153                                                <dt>${artist}</dt><dd>' . $artist . '</dd>
3154                                                <dt>${album}</dt><dd>' . $album . '</dd>
3155                                                <dt>${length}</dt><dd>' . $length . '</dd>
3156                                                <dt>${bitrate}</dt><dd>' . $bitrate . 'kbps</dd>
3157                                        </dl>';
3158                                $content_dl_term = true;
3159                                break;
3160
3161                        case 'video':
3162                                $check_for_embedded_img = true;
3163
3164                                $a_fmt = $this->mkSafeUTF8($this->getID3infoItem($fi, '???', 'audio', 'dataformat'));
3165                                $a_samplerate = round($this->getID3infoItem($fi, 0, 'audio', 'sample_rate') / 1000, 1);
3166                                $a_bitrate = round($this->getID3infoItem($fi, 0, 'audio', 'bitrate') / 1000, 1);
3167                                $a_bitrate_mode = $this->mkSafeUTF8($this->getID3infoItem($fi, '???', 'audio', 'bitrate_mode'));
3168                                $a_channels = round($this->getID3infoItem($fi, 0, 'audio', 'channels'));
3169                                $a_codec = $this->mkSafeUTF8($this->getID3infoItem($fi, '', 'audio', 'codec'));
3170                                $a_streams = $this->getID3infoItem($fi, '???', 'audio', 'streams');
3171                                $a_streamcount = (is_array($a_streams) ? count($a_streams) : 0);
3172
3173                                $v_fmt = $this->mkSafeUTF8($this->getID3infoItem($fi, '???', 'video', 'dataformat'));
3174                                $v_bitrate = round($this->getID3infoItem($fi, 0, 'video', 'bitrate') / 1000, 1);
3175                                $v_bitrate_mode = $this->mkSafeUTF8($this->getID3infoItem($fi, '???', 'video', 'bitrate_mode'));
3176                                $v_framerate = round($this->getID3infoItem($fi, 0, 'video', 'frame_rate'), 5);
3177                                $v_width = round($this->getID3infoItem($fi, '???', 'video', 'resolution_x'));
3178                                $v_height = round($this->getID3infoItem($fi, '???', 'video', 'resolution_y'));
3179                                $v_par = round($this->getID3infoItem($fi, 1.0, 'video', 'pixel_aspect_ratio'), 7);
3180                                $v_codec = $this->mkSafeUTF8($this->getID3infoItem($fi, '', 'video', 'codec'));
3181
3182                                $g_bitrate = round($this->getID3infoItem($fi, 0, 'bitrate') / 1000, 1);
3183                                $g_playtime_str = $this->mkSafeUTF8($this->getID3infoItem($fi, '???', 'playtime_string'));
3184
3185                                $content .= '
3186                                                <dt>Audio</dt><dd>';
3187                                if ($a_fmt === '???' && $a_samplerate == 0 && $a_bitrate == 0 && $a_bitrate_mode === '???' && $a_channels == 0 && empty($a_codec) && $a_streams === '???' && $a_streamcount == 0)
3188                                {
3189                                        $content .= '-';
3190                                }
3191                                else
3192                                {
3193                                        $content .= $a_fmt . (!empty($a_codec) ? ' (' . $a_codec . ')' : '') .
3194                                                                (!empty($a_channels) ? ($a_channels === 1 ? ' (mono)' : ($a_channels === 2 ? ' (stereo)' : ' (' . $a_channels . ' channels)')) : '') .
3195                                                                ': ' . $a_samplerate . ' kHz @ ' . $a_bitrate . ' kbps (' . strtoupper($a_bitrate_mode) . ')' .
3196                                                                ($a_streamcount > 1 ? ' (' . $a_streamcount . ' streams)' : '');
3197                                }
3198                                $content .= '</dd>
3199                                                <dt>Video</dt><dd>' . $v_fmt . (!empty($v_codec) ? ' (' . $v_codec . ')' : '') .  ': ' . $v_framerate . ' fps @ ' . $v_bitrate . ' kbps (' . strtoupper($v_bitrate_mode) . ')' .
3200                                                                                        ($v_par != 1.0 ? ', PAR: ' . $v_par : '') .
3201                                                                        '</dd>
3202                                                <dt>${width}</dt><dd>' . $v_width . 'px</dd>
3203                                                <dt>${height}</dt><dd>' . $v_height . 'px</dd>
3204                                                <dt>${length}</dt><dd>' . $g_playtime_str . '</dd>
3205                                                <dt>${bitrate}</dt><dd>' . $g_bitrate . 'kbps</dd>
3206                                        </dl>';
3207                                $content_dl_term = true;
3208                                break;
3209
3210                        default:
3211                                // fall back to 'no preview available' (if getID3 didn't deliver instead...)
3212                                break;
3213                        }
3214                        break;
3215                }
3216
3217                if (!$content_dl_term)
3218                {
3219                        $content .= '</dl>';
3220                }
3221
3222                if (!empty($fi['error']))
3223                {
3224                        $postdiag_err_HTML .= '<p class="err_info">' . $this->mkSafeUTF8(implode(', ', $fi['error'])) . '</p>';
3225                }
3226
3227                $emsgX = null;
3228                if (empty($thumb250))
3229                {
3230                        if (!$thumbnails_done_or_deferred)
3231                        {
3232                                // check if we have stored a thumbnail for this file anyhow:
3233                                $thumb250 = $this->getThumb($meta, $file, $this->options['thumbBigSize'], $this->options['thumbBigSize'], true);
3234                                if (empty($thumb250))
3235                                {
3236                                        if (!empty($fi) && $check_for_embedded_img)
3237                                        {
3238                                                /*
3239                                                 * No thumbnail available yet, so find me one!
3240                                                 *
3241                                                 * When we find a thumbnail during the 'cleanup' scan, we don't know up front if it's suitable to be used directly,
3242                                                 * so we treat it as an alternative 'original' file and generate a 250px/48px thumbnail set from it.
3243                                                 *
3244                                                 * When the embedded thumbnail is small enough, the thumbnail creation process will be simply a copy action, so relatively
3245                                                 * low cost.
3246                                                 */
3247                                                $embed = $this->extract_ID3info_embedded_image($fi);
3248                                                //@file_put_contents(dirname(__FILE__) . '/extract_embedded_img.log', print_r(array('html' => $preview_HTML, 'json' => $json, 'thumb250_e' => $thumb250_e, 'thumb250' => $thumb250, 'embed' => $embed, 'fileinfo' => $fi), true));
3249                                                if (is_object($embed))
3250                                                {
3251                                                        $thumbX = $meta->getThumbURL('embed');
3252                                                        $tfi = pathinfo($thumbX);
3253                                                        $tfi['extension'] = image_type_to_extension($embed->metadata[2]);
3254                                                        $thumbX = $tfi['dirname'] . '/' . $tfi['filename'] . '.' . $tfi['extension'];
3255                                                        $thumbX = $this->normalize($thumbX);
3256                                                        $thumbX_f = $this->url_path2file_path($thumbX);
3257                                                        // as we've spent some effort to dig out the embedded thumbnail, and 'knowing' (assuming) that generally
3258                                                        // embedded thumbnails are not too large, we don't concern ourselves with delaying the thumbnail generation (the
3259                                                        // source file mapping is not bidirectional, either!) and go straight ahead and produce the 250px thumbnail at least.
3260                                                        $thumb250   = false;
3261                                                        $thumb250_e = false;
3262                                                        $thumb48    = false;
3263                                                        $thumb48_e  = false;
3264                                                        $meta->mkCacheDir();
3265                                                        if (false === file_put_contents($thumbX_f, $embed->imagedata))
3266                                                        {
3267                                                                @unlink($thumbX_f);
3268                                                                $emsgX = 'Cannot save embedded image data to cache.';
3269                                                                $icon48 = $this->getIcon('is.default-error', false);
3270                                                                $icon = $this->getIcon('is.default-error', true);
3271                                                        }
3272                                                        else
3273                                                        {
3274                                                                try
3275                                                                {
3276                                                                        $thumb250 = $this->getThumb($meta, $thumbX_f, $this->options['thumbBigSize'], $this->options['thumbBigSize'], false);
3277                                                                        if (!empty($thumb250))
3278                                                                        {
3279                                                                                $thumb250_e = FileManagerUtility::rawurlencode_path($thumb250);
3280                                                                        }
3281                                                                        $thumb48 = $this->getThumb($meta, (!empty($thumb250) ? $this->url_path2file_path($thumb250) : $thumbX_f), $this->options['thumbSmallSize'], $this->options['thumbSmallSize'], false);
3282                                                                        if (!empty($thumb48))
3283                                                                        {
3284                                                                                $thumb48_e = FileManagerUtility::rawurlencode_path($thumb48);
3285                                                                        }
3286                                                                }
3287                                                                catch (Exception $e)
3288                                                                {
3289                                                                        $emsgX = $e->getMessage();
3290                                                                        $icon48 = $this->getIconForError($emsgX, $legal_url, false);
3291                                                                        $icon = $this->getIconForError($emsgX, $legal_url, true);
3292                                                                }
3293                                                        }
3294                                                }
3295                                        }
3296                                }
3297                                else
3298                                {
3299                                        // !empty($thumb250)
3300                                        $thumb250_e = FileManagerUtility::rawurlencode_path($thumb250);
3301                                        try
3302                                        {
3303                                                $thumb48 = $this->getThumb($meta, $this->url_path2file_path($thumb250), $this->options['thumbSmallSize'], $this->options['thumbSmallSize'], false);
3304                                                assert(!empty($thumb48));
3305                                                $thumb48_e = FileManagerUtility::rawurlencode_path($thumb48);
3306                                        }
3307                                        catch (Exception $e)
3308                                        {
3309                                                $emsgX = $e->getMessage();
3310                                                $icon48 = $this->getIconForError($emsgX, $legal_url, false);
3311                                                $icon = $this->getIconForError($emsgX, $legal_url, true);
3312                                                $thumb48 = false;
3313                                                $thumb48_e = false;
3314                                        }
3315                                }
3316                        }
3317                }
3318                else // if (!empty($thumb250))
3319                {
3320                        if (empty($thumb250_e))
3321                        {
3322                                $thumb250_e = FileManagerUtility::rawurlencode_path($thumb250);
3323                        }
3324                        if (empty($thumb48))
3325                        {
3326                                try
3327                                {
3328                                        $thumb48 = $this->getThumb($meta, $this->url_path2file_path($thumb250), $this->options['thumbSmallSize'], $this->options['thumbSmallSize'], false);
3329                                        assert(!empty($thumb48));
3330                                        $thumb48_e = FileManagerUtility::rawurlencode_path($thumb48);
3331                                }
3332                                catch (Exception $e)
3333                                {
3334                                        $emsgX = $e->getMessage();
3335                                        $icon48 = $this->getIconForError($emsgX, $legal_url, false);
3336                                        $icon = $this->getIconForError($emsgX, $legal_url, true);
3337                                        $thumb48 = false;
3338                                        $thumb48_e = false;
3339                                }
3340                        }
3341                        if (empty($thumb48_e))
3342                        {
3343                                $thumb48_e = FileManagerUtility::rawurlencode_path($thumb48);
3344                        }
3345                }
3346
3347                // also provide X/Y size info with each direct-access thumbnail file:
3348                if (!empty($thumb250))
3349                {
3350                        $json['thumb250'] = $thumb250_e;
3351                        $meta->store('thumb250_direct', $thumb250);
3352
3353                        $tnsize = $meta->fetch('thumb250_info');
3354                        if (empty($tnsize))
3355                        {
3356                                $tnsize = getimagesize($this->url_path2file_path($thumb250));
3357                                $meta->store('thumb250_info', $tnsize);
3358                        }
3359                        if (is_array($tnsize))
3360                        {
3361                                $json['thumb250_width'] = $tnsize[0];
3362                                $json['thumb250_height'] = $tnsize[1];
3363
3364                                if (empty($preview_HTML))
3365                                {
3366                                        $preview_HTML = '<a href="' . FileManagerUtility::rawurlencode_path($url) . '" data-milkbox="single" title="' . htmlentities($filename, ENT_QUOTES, 'UTF-8') . '">
3367                                                                           <img src="' . $thumb250_e . '" class="preview" alt="' . (!empty($emsgX) ? $this->mkSafe4HTMLattr($emsgX) : 'preview') . '"
3368                                                                                        style="width: ' . $tnsize[0] . 'px; height: ' . $tnsize[1] . 'px;" />
3369                                                                         </a>';
3370                                }
3371                        }
3372                }
3373                if (!empty($thumb48))
3374                {
3375                        $json['thumb48'] = $thumb48_e;
3376                        $meta->store('thumb48_direct', $thumb48);
3377
3378                        $tnsize = $meta->fetch('thumb48_info');
3379                        if (empty($tnsize))
3380                        {
3381                                $tnsize = getimagesize($this->url_path2file_path($thumb48));
3382                                $meta->store('thumb48_info', $tnsize);
3383                        }
3384                        if (is_array($tnsize))
3385                        {
3386                                $json['thumb48_width'] = $tnsize[0];
3387                                $json['thumb48_height'] = $tnsize[1];
3388                        }
3389                }
3390                if ($thumbnails_done_or_deferred && (empty($thumbs250) || empty($thumbs48)))
3391                {
3392                        $json['thumbs_deferred'] = true;
3393                }
3394                else
3395                {
3396                        $json['thumbs_deferred'] = false;
3397                }
3398
3399                if (!empty($icon48))
3400                {
3401                        $icon48_e = FileManagerUtility::rawurlencode_path($icon48);
3402                        $json['icon48'] = $icon48_e;
3403                }
3404                if (!empty($icon))
3405                {
3406                        $icon_e = FileManagerUtility::rawurlencode_path($icon);
3407                        $json['icon'] = $icon_e;
3408                }
3409
3410                $fi4dump = null;
3411                if (!empty($fi))
3412                {
3413                        try
3414                        {
3415                                $fi4dump = $meta->fetch('file_info_dump');
3416                                if (empty($fi4dump))
3417                                {
3418                                        $fi4dump = array_merge(array(), $fi); // clone $fi
3419                                        $this->clean_ID3info_results($fi4dump);
3420                                        $meta->store('file_info_dump', $fi4dump);
3421                                }
3422
3423                                $dump = FileManagerUtility::table_var_dump($fi4dump, false);
3424
3425                                $postdiag_dump_HTML .= "\n" . $dump . "\n";
3426                                //@file_put_contents(dirname(__FILE__) . '/getid3.log', print_r(array('html' => $preview_HTML, 'json' => $json, 'thumb250_e' => $thumb250_e, 'thumb250' => $thumb250, 'embed' => $embed, 'fileinfo' => $fi), true));
3427                        }
3428                        catch(Exception $e)
3429                        {
3430                                $postdiag_err_HTML .= '<p class="err_info">' . $e->getMessage() . '</p>';
3431                        }
3432                }
3433
3434                if ($preview_HTML === null)
3435                {
3436                        $preview_HTML = '${nopreview}';
3437                }
3438
3439                if (!empty($preview_HTML))
3440                {
3441                        //$content .= '<h3>${preview}</h3>';
3442                        $content .= '<div class="filemanager-preview-content">' . $preview_HTML . '</div>';
3443                }
3444                if (!empty($postdiag_err_HTML))
3445                {
3446                        $content .= '<div class="filemanager-errors">' . $postdiag_err_HTML . '</div>';
3447                }
3448                if (!empty($postdiag_dump_HTML) && $metaHTML_mode)
3449                {
3450                        $content .= '<div class="filemanager-diag-dump">' . $postdiag_dump_HTML . '</div>';
3451                }
3452
3453                $json['content'] = self::compressHTML($content);
3454                $json['metadata'] = ($metaJSON_mode ? $fi4dump : null);
3455
3456                return array_merge((is_array($json_in) ? $json_in : array()), $json);
3457        }
3458
3459        /**
3460         * Traverse the getID3 info[] array tree and fetch the item pointed at by the variable number of indices specified
3461         * as additional parameters to this function.
3462         *
3463         * Return the default value when the indicated element does not exist in the info[] set; otherwise return the located item.
3464         *
3465         * The purpose of this method is to act as a safe go-in-between for the fileManager to collect arbitrary getID3 data and
3466         * not get a PHP error when some item in there does not exist.
3467         */
3468        public /* static */ function getID3infoItem($getid3_info_obj, $default_value /* , ... */ )
3469        {
3470                $rv = false;
3471                $argc = func_num_args();
3472
3473                for ($i = 2; $i < $argc; $i++)
3474                {
3475      $index = func_get_arg($i);
3476                        if (!is_array($getid3_info_obj))
3477                        {
3478        // the getID3 library seems to return an array for id3 tags, the fall back does not, we
3479        // will just short circuit this, it'll be fine.
3480        if($i == $argc-1 && $index === 0) return $getid3_info_obj;
3481       
3482                                return $default_value;
3483                        }
3484
3485                        if (array_key_exists($index, $getid3_info_obj))
3486                        {
3487                                $getid3_info_obj = $getid3_info_obj[$index];
3488                        }
3489                        else
3490                        {
3491                                return $default_value;
3492                        }
3493                }
3494                // WARNING: convert '$' to the HTML entity to prevent the JS/client side from 'seeing' the $ and start ${xyz} template variable replacement erroneously
3495                return str_replace('$', '&#36;', $getid3_info_obj);
3496        }
3497
3498  /** Return an array of information about a file.
3499   *
3500   *    IFF the getID3 library (GPL) is available, it will be used.
3501   *    ELSE IF FileInfo extention is available, we will try that for getting mimetype
3502   *    ELSE do it manually
3503   *
3504   *  == All Files ==
3505   *  mime_type
3506   *
3507   *  == Images ==   
3508   *  video:[resolution_x, resolution_y]
3509   *  jpg:exif:IFD0:[Software, DateTime]
3510   *
3511   *  == Zip Files ==
3512   *  zip:files (an array (name => size|files))
3513   *
3514   *  == Flash Files ==
3515   *  swf:header:[frame_width, frame_height, frame_count, length]
3516   *
3517   *  == Audio Files ==
3518   *  tags:id3v1:[title,artist,album]   
3519   *  playtime_string
3520   *  bitrate
3521   *
3522   *  == Video Files ==
3523   *  audio:[dataformat,sample_rate,bitrate,bitrate_mode,channels,codec,streams]
3524   *  video:[dataformat,bitrate,bitrate_mode,frame_rate,resolution_x,resolution_y,pixel_aspect_ratio,codec]
3525   *  bitrate
3526   *  playtime_string
3527   */
3528   
3529  public function analyze_file($file, $legal_url)
3530  {
3531    if($this->options['useGetID3IfAvailable'] && (class_exists('getID3') || file_exists(strtr(dirname(__FILE__), '\\', '/') . '/Assets/getid3/getid3.php')))
3532    {
3533      // Note delaying requiring getiD3 until now, because it is big and we don't need it if the data is cached already.
3534      if(!class_exists('GetId3'))
3535      {
3536        require(strtr(dirname(__FILE__), '\\', '/') . '/Assets/getid3/getid3.php');
3537      }
3538
3539      $id3 = new getID3();
3540      $id3->setOption(array('encoding' => 'UTF-8'));
3541      $id3->analyze($file);     
3542      $rv = $id3->info;
3543      if (empty($rv['mime_type']))
3544      {
3545        // guarantee to produce a mime type, at least!
3546        $rv['mime_type'] = $this->getMimeFromExt($legal_url);     // guestimate mimetype when content sniffing didn't work
3547      }
3548     
3549      return $rv;
3550    }
3551    else
3552    {
3553      $rv = array();
3554     
3555      if(function_exists('finfo_open') && defined('FILEINFO_MIME_TYPE'))
3556      {
3557        if(!isset($this->_finfo)) $this->_finfo = finfo_open(FILEINFO_MIME_TYPE);
3558        $rv['mime_type'] = finfo_file($this->_finfo, $file);       
3559      }
3560     
3561      if(!@$rv['mime_type'])
3562      {
3563        $rv['mime_type'] = $this->getMimeFromExt($legal_url);     // guestimate mimetype when content sniffing didn't work
3564      }
3565                 
3566      switch(preg_replace('/\/.*$/', '', $rv['mime_type']))
3567      {
3568        case 'image':
3569        {
3570          $size = getimagesize($file);
3571          if($size !== FALSE)
3572          {
3573            $rv['video']['resolution_x'] = $size[0];
3574            $rv['video']['resolution_y'] = $size[1];                       
3575          }
3576         
3577          if(function_exists('exif_imagetype') && exif_imagetype($file) !== FALSE)
3578          {
3579            $rv['exif'] = @exif_read_data($file, 0, true);   
3580            $rv['jpg']['exif'] = $rv['exif']; // Not strictly true!
3581          }
3582        }
3583        break;
3584       
3585        case 'audio':
3586        {
3587          switch($rv['mime_type'])
3588          {
3589            case 'audio/mpeg':
3590            {
3591              require_once(strtr(dirname(__FILE__), '\\', '/') . '/ID3.class.php');
3592              $ID3 = new id3Parser();
3593              $tags = $ID3->read($file);
3594              switch($tags['id3'])
3595              {               
3596                case 1: $rv['tags']['id3v1'] = $tags;
3597                default: $rv['tags']['id3v2'] = $tags;
3598              }
3599             
3600            }           
3601            break;
3602          }
3603        }
3604        break;
3605      }
3606     
3607      return $rv;
3608    }
3609  }
3610
3611
3612
3613        /**
3614         * Extract an embedded image from the getID3 info data.
3615         *
3616         * Return FALSE when no embedded image was found, otherwise return an array of the metadata and the binary image data itself.
3617         */
3618        protected function extract_ID3info_embedded_image(&$arr)
3619        {
3620                if (is_array($arr))
3621                {
3622                        foreach ($arr as $key => &$value)
3623                        {
3624                                if ($key === 'data' && isset($arr['image_mime']))
3625                                {
3626                                        // first make sure this is a valid image
3627                                        $imageinfo = array();
3628                                        $imagechunkcheck = getid3_lib::GetDataImageSize($value, $imageinfo);
3629                                        if (is_array($imagechunkcheck) && isset($imagechunkcheck[2]) && in_array($imagechunkcheck[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG)))
3630                                        {
3631                                                return new EmbeddedImageContainer($imagechunkcheck, $value);
3632                                        }
3633                                }
3634                                else if (is_array($value))
3635                                {
3636                                        $rv = $this->extract_ID3info_embedded_image($value);
3637                                        if ($rv !== false)
3638                                                return $rv;
3639                                }
3640                        }
3641                }
3642                return false;
3643        }
3644
3645        protected function fold_quicktime_subatoms(&$arr, &$inject, $key_prefix)
3646        {
3647                $satms = false;
3648                if (!empty($arr['subatoms']) && is_array($arr['subatoms']) && !empty($arr['hierarchy']) && !empty($arr['name']) && $arr['hierarchy'] == $arr['name'])
3649                {
3650                        // fold these up all the way to the root:
3651                        $key_prefix .= '.' . $arr['hierarchy'];
3652
3653                        $satms = $arr['subatoms'];
3654                        unset($arr['subatoms']);
3655                        $inject[$key_prefix] = $satms;
3656                }
3657
3658                foreach ($arr as $key => &$value)
3659                {
3660                        if (is_array($value))
3661                        {
3662                                $this->fold_quicktime_subatoms($value, $inject, $key_prefix);
3663                        }
3664                }
3665
3666                if ($satms !== false)
3667                {
3668                        $this->fold_quicktime_subatoms($inject[$key_prefix], $inject, $key_prefix);
3669                }
3670        }
3671
3672        // assumes an input array with the keys in CamelCase semi-Hungarian Notation. Convert those keys to something humanly grokkable after it is processed by table_var_dump().
3673        protected function clean_AVI_Hungarian(&$arr)
3674        {
3675                $dst = array();
3676                foreach($arr as $key => &$value)
3677                {
3678                        $nk = explode('_', preg_replace('/([A-Z])/', '_\\1', $key));
3679                        switch ($nk[0])
3680                        {
3681                        case 'dw':
3682                        case 'n':
3683                        case 'w':
3684                        case 'bi':
3685                                unset($nk[0]);
3686                                break;
3687                        }
3688                        $dst[strtolower(implode('_', $nk))] = $value;
3689                }
3690                $arr = $dst;
3691        }
3692
3693        // another round of scanning to rewrite the keys to human legibility: as this changes the keys, we'll need to rewrite all entries to keep order intact
3694        protected function clean_ID3info_keys(&$arr)
3695        {
3696                $dst = array();
3697                foreach($arr as $key => &$value)
3698                {
3699                        $key = strtr($key, "_\x00", '  ');
3700
3701                        // custom transformations: (hopefully switch/case/case/... is faster than str_replace/strtr here)
3702                        switch ((string)$key)
3703                        {
3704                        default:
3705                                //$key = $this->mkSafeUTF8($key);
3706                                if (preg_match('/[^ -~]/', $key))
3707                                {
3708                                        // non-ASCII values in the key: hexdump those characters!
3709                                        $klen = strlen($key);
3710                                        $nk = '';
3711                                        for ($i = 0; $i < $klen; ++$i)
3712                                        {
3713                                                $c = ord($key[$i]);
3714
3715                                                if ($c >= 32 && $c <= 127)
3716                                                {
3717                                                        $nk .= chr($c);
3718                                                }
3719                                                else
3720                                                {
3721                                                        $nk .= sprintf('$%02x', $c);
3722                                                }
3723                                        }
3724                                        $key = $nk;
3725                                }
3726                                break;
3727
3728                        case 'avdataend':
3729                                $key = 'AV data end';
3730                                break;
3731
3732                        case 'avdataoffset':
3733                                $key = 'AV data offset';
3734                                break;
3735
3736                        case 'channelmode':
3737                                $key = 'channel mode';
3738                                break;
3739
3740                        case 'dataformat':
3741                                $key = 'data format';
3742                                break;
3743
3744                        case 'fileformat':
3745                                $key = 'file format';
3746                                break;
3747
3748                        case 'modeextension':
3749                                $key = 'mode extension';
3750                                break;
3751                        }
3752
3753                        $dst[$key] = $value;
3754                        if (is_array($value))
3755                        {
3756                                $this->clean_ID3info_keys($dst[$key]);
3757                        }
3758                }
3759                $arr = $dst;
3760        }
3761
3762        protected function clean_ID3info_results_r(&$arr, $flags)
3763        {
3764                if (is_array($arr))
3765                {
3766                        // heuristic #1: fold all the quickatoms subatoms using their hierarchy and name fields; this is a tree rewrite
3767                        if (array_key_exists('quicktime', $arr) && is_array($arr['quicktime']))
3768                        {
3769                                $inject = array();
3770                                $this->fold_quicktime_subatoms($arr['quicktime'], $inject, 'quicktime');
3771
3772                                // can't use array_splice on associative arrays, so we rewrite $arr now:
3773                                $newarr = array();
3774                                foreach ($arr as $key => &$value)
3775                                {
3776                                        $newarr[$key] = $value;
3777                                        if ($key === 'quicktime')
3778                                        {
3779                                                foreach ($inject as $ik => &$iv)
3780                                                {
3781                                                        $newarr[$ik] = $iv;
3782                                                }
3783                                        }
3784                                }
3785                                $arr = $newarr;
3786                                unset($inject);
3787                                unset($newarr);
3788                        }
3789
3790                        $activity = true;
3791                        while ($activity)
3792                        {
3793                                $activity = false; // assume there's nothing to do anymore. Prove us wrong now...
3794
3795                                // heuristic #2: when the number of items in the array which are themselves arrays is 80%, contract the set
3796                                $todo = array();
3797                                foreach ($arr as $key => &$value)
3798                                {
3799                                        if (is_array($value))
3800                                        {
3801                                                $acnt = 0;
3802                                                foreach ($value as $sk => &$sv)
3803                                                {
3804                                                        if (is_array($sv))
3805                                                        {
3806                                                                $acnt++;
3807                                                        }
3808                                                }
3809
3810                                                // the floor() here helps to fold single-element arrays alongside! :-)
3811                                                if (floor(0.5 * count($value)) <= $acnt)
3812                                                {
3813                                                        $todo[] = $key;
3814                                                }
3815                                        }
3816                                }
3817                                if (count($todo) > 0)
3818                                {
3819                                        $inject = array();
3820                                        foreach ($arr as $key => &$value)
3821                                        {
3822                                                if (is_array($value) && in_array($key, $todo))
3823                                                {
3824                                                        unset($todo[$key]);
3825
3826                                                        foreach ($value as $sk => &$sv)
3827                                                        {
3828                                                                $nk = $key . '.' . $sk;
3829
3830                                                                // pull up single entry subsubarrays at the same time!
3831                                                                if (is_array($sv) && count($sv) == 1)
3832                                                                {
3833                                                                        foreach ($sv as $sk2 => &$sv2)
3834                                                                        {
3835                                                                                $nk .= '.' . $sk2;
3836                                                                                $inject[$nk] = $sv2;
3837                                                                        }
3838                                                                }
3839                                                                else
3840                                                                {
3841                                                                        $inject[$nk] = $sv;
3842                                                                }
3843                                                        }
3844                                                }
3845                                                else
3846                                                {
3847                                                        $inject[$key] = $value;
3848                                                }
3849                                        }
3850                                        $arr = $inject;
3851                                        $activity = true;
3852                                }
3853                        }
3854
3855                        foreach ($arr as $key => &$value)
3856                        {
3857                                if ($key === 'data' && isset($arr['image_mime']))
3858                                {
3859                                        // when this is a valid image, it's already available as a thumbnail, most probably
3860                                        $imageinfo = array();
3861                                        $imagechunkcheck = getid3_lib::GetDataImageSize($value, $imageinfo);
3862                                        if (is_array($imagechunkcheck) && isset($imagechunkcheck[2]) && in_array($imagechunkcheck[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG)))
3863                                        {
3864                                                if ($flags & MTFM_CLEAN_ID3_STRIP_EMBEDDED_IMAGES)
3865                                                {
3866                                                        $value = new BinaryDataContainer('(embedded image ' . image_type_to_extension($imagechunkcheck[2]) . ' data...)');
3867                                                }
3868                                                else
3869                                                {
3870                                                        $value = new EmbeddedImageContainer($imagechunkcheck, $value);
3871                                                }
3872                                        }
3873                                        else
3874                                        {
3875                                                if ($flags & MTFM_CLEAN_ID3_STRIP_EMBEDDED_IMAGES)
3876                                                {
3877                                                        $value = new BinaryDataContainer('(unidentified image data ... ' . (is_string($value) ? 'length = ' . strlen($value) : '') . ' -- ' . print_r($imagechunkcheck) . ')');
3878                                                }
3879                                                else
3880                                                {
3881                                                        $this->clean_ID3info_results_r($value, $flags);
3882                                                }
3883                                        }
3884                                }
3885                                else if (isset($arr[$key . '_guid']))
3886                                {
3887                                        // convert guid raw binary data to hex:
3888                                        $temp = unpack('H*', $value);
3889                                        $value = new BinaryDataContainer($temp[1]);
3890                                }
3891                                else if (  $key === 'non_intra_quant'                                                                       // MPEG quantization matrix
3892                                                || $key === 'error_correct_type'
3893                                                )
3894                                {
3895                                        // convert raw binary data to hex in 32 bit chunks:
3896                                        $temp = unpack('H*', $value);
3897                                        $temp = str_split($temp[1], 8);
3898                                        $value = new BinaryDataContainer(implode(' ', $temp));
3899                                }
3900                                else if ($key === 'data' && is_string($value) && isset($arr['frame_name']) && isset($arr['encoding']) && isset($arr['datalength'])) // MP3 tag chunk
3901                                {
3902                                        $str = $this->mkSafeUTF8(trim(strtr(getid3_lib::iconv_fallback($arr['encoding'], 'UTF-8', $value), "\x00", ' ')));
3903                                        $temp = unpack('H*', $value);
3904                                        $temp = str_split($temp[1], 8);
3905                                        $value = new BinaryDataContainer(implode(' ', $temp) . (!empty($str) ? ' (' . $str . ')' : ''));
3906                                }
3907                                else if (  ($key === 'data' && is_string($value) && isset($arr['offset']) && isset($arr['size']))           // AVI offset/size/data items: data = binary
3908                                                || ($key === 'type_specific_data' && is_string($value) /* && isset($arr['type_specific_len']) */ )  // munch WMV/RM 'type specific data': binary   ('type specific len' will occur alongside, but not in WMV)
3909                                                )
3910                                {
3911                                        // a bit like UNIX strings tool: strip out anything which isn't at least possibly legible
3912                                        $str = ' ' . preg_replace('/[^ !#-~]/', ' ', strtr($value, "\x00", ' ')) . ' ';     // convert non-ASCII and double quote " to space
3913                                        do
3914                                        {
3915                                                $repl_count = 0;
3916                                                $str = preg_replace(array('/ [^ ] /',
3917                                                                                                  '/ [^A-Za-z0-9\\(.]/',
3918                                                                                                  '/ [A-Za-z0-9][^A-Za-z0-9\\(. ] /'
3919                                                                                                 ), ' ', $str, -1, $repl_count);
3920                                        } while ($repl_count > 0);
3921                                        $str = trim($str);
3922
3923                                        if (strlen($value) <= 256)
3924                                        {
3925                                                $temp = unpack('H*', $value);
3926                                                $temp = str_split($temp[1], 8);
3927                                                $temp = implode(' ', $temp);
3928                                                $value = new BinaryDataContainer($temp . (!empty($str) > 0 ? ' (' . $str . ')' : ''));
3929                                        }
3930                                        else
3931                                        {
3932                                                $value = new BinaryDataContainer('binary data... (length = ' . strlen($value) . ' bytes)');
3933                                        }
3934                                }
3935                                else if (is_scalar($value) && preg_match('/^(dw[A-Z]|n[A-Z]|w[A-Z]|bi[A-Z])[a-zA-Z]+$/', $key))
3936                                {
3937                                        // AVI sections which use Hungarian notation, at least partially
3938                                        $this->clean_AVI_Hungarian($arr);
3939                                        // and rescan the transformed key set...
3940                                        $this->clean_ID3info_results($arr);
3941                                        break; // exit this loop
3942                                }
3943                                else if (is_array($value))
3944                                {
3945                                        // heuristic #3: when the value is an array of integers, implode them to a comma-separated list (string) instead:
3946                                        $is_all_ints = true;
3947                                        for ($sk = count($value) - 1; $sk >= 0; --$sk)
3948                                        {
3949                                                if (!array_key_exists($sk, $value) || !is_int($value[$sk]))
3950                                                {
3951                                                        $is_all_ints = false;
3952                                                        break;
3953                                                }
3954                                        }
3955                                        if ($is_all_ints)
3956                                        {
3957                                                $s = implode(', ', $value);
3958                                                $value = $s;
3959                                        }
3960                                        else
3961                                        {
3962                                                $this->clean_ID3info_results_r($value, $flags);
3963                                        }
3964                                }
3965                                else
3966                                {
3967                                        $this->clean_ID3info_results_r($value, $flags);
3968                                }
3969                        }
3970                }
3971                else if (is_string($arr))
3972                {
3973                        // is this a cleaned up item? Yes, then there's a full-ASCII string here, sans newlines, etc.
3974                        $len = strlen($arr);
3975                        $value = rtrim($arr, "\x00");
3976                        $value = strtr($value, "\x00", ' ');    // because preg_match() doesn't 'see' NUL bytes...
3977                        if (preg_match("/[^ -~\n\r\t]/", $value))
3978                        {
3979                                if ($len > 0 && $len < 256)
3980                                {
3981                                        // check if we can turn it into something UTF8-LEGAL; when not, we hexdump!
3982                                        $im = str_replace('?', '&QMaRK;', $value);
3983                                        $dst = $this->mkSafeUTF8($im);
3984                                        if (strpos($dst, '?') === false)
3985                                        {
3986                                                // it's a UTF-8 legal string now!
3987                                                $arr = str_replace('&QMaRK;', '?', $dst);
3988                                        }
3989                                        else
3990                                        {
3991                                                // convert raw binary data to hex in 32 bit chunks:
3992                                                $temp = unpack('H*', $arr);
3993                                                $temp = str_split($temp[1], 8);
3994                                                $arr = new BinaryDataContainer(implode(' ', $temp));
3995                                        }
3996                                }
3997                                else
3998                                {
3999                                        $arr = new BinaryDataContainer('(unidentified binary data ... length = ' . strlen($arr) . ')');
4000                                }
4001                        }
4002                        else
4003                        {
4004                                $arr = $value;   // don't store as a 'processed' item (shortcut)
4005                        }
4006                }
4007                else if (is_bool($arr) ||
4008                                 is_int($arr) ||
4009                                 is_float($arr) ||
4010                                 is_null($arr))
4011                {
4012                }
4013                else if (is_object($arr) && !isset($arr->id3_procsupport_obj))
4014                {
4015                        $arr = new BinaryDataContainer('(object) ' . $this->mkSafeUTF8(print_r($arr, true)));
4016                }
4017                else if (is_resource($arr))
4018                {
4019                        $arr = new BinaryDataContainer('(resource) ' . $this->mkSafeUTF8(print_r($arr, true)));
4020                }
4021                else
4022                {
4023                        $arr = new BinaryDataContainer('(unidentified type: ' . gettype($arr) . ') ' . $this->mkSafeUTF8(print_r($arr, true)));
4024                }
4025        }
4026
4027        protected function clean_ID3info_results(&$arr, $flags = 0)
4028        {
4029                unset($arr['GETID3_VERSION']);
4030                unset($arr['filepath']);
4031                unset($arr['filename']);
4032                unset($arr['filenamepath']);
4033                unset($arr['cache_hash']);
4034                unset($arr['cache_file']);
4035
4036                $this->clean_ID3info_results_r($arr, $flags);
4037
4038                // heuristic #4: convert keys to something legible:
4039                if (is_array($arr))
4040                {
4041                        $this->clean_ID3info_keys($arr);
4042                }
4043        }
4044
4045
4046
4047
4048
4049
4050
4051
4052        /**
4053         * Delete a file or directory, inclusing subdirectories and files.
4054         *
4055         * Return TRUE on success, FALSE when an error occurred.
4056         *
4057         * Note that the routine will try to persevere and keep deleting other subdirectories
4058         * and files, even when an error occurred for one or more of the subitems: this is
4059         * a best effort policy.
4060         */
4061        protected function unlink($legal_url, $mime_filters)
4062        {
4063                $rv = true;
4064
4065                // must transform here so alias/etc. expansions inside legal_url_path2file_path() get a chance:
4066                $file = $this->legal_url_path2file_path($legal_url);
4067
4068                if (is_dir($file))
4069                {
4070                        $dir = self::enforceTrailingSlash($file);
4071                        $url = self::enforceTrailingSlash($legal_url);
4072                        $coll = $this->scandir($dir, '*', false, 0, ~GLOB_NOHIDDEN);
4073                        if ($coll !== false)
4074                        {
4075                                foreach ($coll['dirs'] as $f)
4076                                {
4077                                        if ($f === '.' || $f === '..')
4078                                                continue;
4079
4080                                        $rv &= $this->unlink($url . $f, $mime_filters);
4081                                }
4082                                foreach ($coll['files'] as $f)
4083                                {
4084                                        $rv &= $this->unlink($url . $f, $mime_filters);
4085                                }
4086                        }
4087                        else
4088                        {
4089                                $rv = false;
4090                        }
4091
4092                        $rv &= @rmdir($file);
4093                }
4094                else if (file_exists($file))
4095                {
4096                        if (is_file($file))
4097                        {
4098                                $mime = $this->getMimeFromExt($file);   // take the fast track to mime type sniffing; we'll live with the (rather low) risk of being inacurate due to accidental/intentional misnaming of the files
4099                                if (!$this->IsAllowedMimeType($mime, $mime_filters))
4100                                        return false;
4101                        }
4102
4103                        $meta = &$this->getid3_cache->pick($legal_url, $this, false);
4104                        assert($meta != null);
4105
4106                        $rv &= @unlink($file);
4107                        $rv &= $meta->delete(true);
4108
4109                        unset($meta);
4110                }
4111                return $rv;
4112        }
4113
4114        /**
4115         * glob() wrapper: accepts the same options as Tooling.php::safe_glob()
4116         *
4117         * However, this method will also ensure the '..' directory entry is only returned,
4118         * even while asked for, when the parent directory can be legally traversed by the FileManager.
4119         *
4120         * Return a dual array (possibly empty) of directories and files, or FALSE on error.
4121         *
4122         * IMPORTANT: this function GUARANTEES that, when present at all, the double-dot '..'
4123         *            entry is the very last entry in the array.
4124         *            This guarantee is important as onView() et al depend on it.
4125         */
4126        public function scandir($dir, $filemask, $see_thumbnail_dir, $glob_flags_or, $glob_flags_and)
4127        {
4128                // list files, except the thumbnail folder itself or any file in it:
4129                $dir = self::enforceTrailingSlash($dir);
4130
4131                $just_below_thumbnail_dir = false;
4132                if (!$see_thumbnail_dir)
4133                {
4134                        $tnpath = $this->thumbnailCacheDir;
4135                        if (FileManagerUtility::startswith($dir, $tnpath))
4136                                return false;
4137
4138                        $tnparent = $this->thumbnailCacheParentDir;
4139                        $just_below_thumbnail_dir = ($dir == $tnparent);
4140
4141                        $tndir = basename(substr($this->options['thumbnailPath'], 0, -1));
4142                }
4143
4144                $at_basedir = ($this->managedBaseDir == $dir);
4145
4146                $flags = GLOB_NODOTS | GLOB_NOHIDDEN | GLOB_NOSORT;
4147                $flags &= $glob_flags_and;
4148                $flags |= $glob_flags_or;
4149                $coll = safe_glob($dir . $filemask, $flags);
4150
4151                if ($coll !== false)
4152                {
4153                        if ($just_below_thumbnail_dir)
4154                        {
4155                                foreach($coll['dirs'] as $k => $dir)
4156                                {
4157                                        if ($dir === $tndir)
4158                                        {
4159                                                unset($coll['dirs'][$k]);
4160                                                break;
4161                                        }
4162                                }
4163                        }
4164
4165                        if (!$at_basedir)
4166                        {
4167                                $coll['dirs'][] = '..';
4168                        }
4169                }
4170
4171                return $coll;
4172        }
4173
4174
4175
4176        /**
4177         * Check the $extension argument and replace it with a suitable 'safe' extension.
4178         */
4179        public function getSafeExtension($extension, $safe_extension = 'txt', $mandatory_extension = 'txt')
4180        {
4181                if (!is_string($extension) || $extension === '') // can't use 'empty($extension)' as "0" is a valid extension itself.
4182                {
4183                        //enforce a mandatory extension, even when there isn't one (due to filtering or original input producing none)
4184                        return (!empty($mandatory_extension) ? $mandatory_extension : (!empty($safe_extension) ? $safe_extension : $extension));
4185                }
4186                $extension = strtolower($extension);
4187                switch ($extension)
4188                {
4189                case 'exe':
4190                case 'dll':
4191                case 'com':
4192                case 'sys':
4193                case 'bat':
4194                case 'pl':
4195                case 'sh':
4196                case 'php':
4197                case 'php3':
4198                case 'php4':
4199                case 'php5':
4200                case 'phps':
4201                        return (!empty($safe_extension) ? $safe_extension : $extension);
4202
4203                default:
4204                        return $extension;
4205                }
4206        }
4207
4208        /**
4209         * Only allow a 'dotted', i.e. UNIX hidden filename when options['safe'] == FALSE
4210         */
4211        public function IsHiddenNameAllowed($file)
4212        {
4213                if ($this->options['safe'] && !empty($file))
4214                {
4215                        if ($file !== '.' && $file !== '..' && $file[0] === '.')
4216                        {
4217                                return false;
4218                        }
4219                }
4220                return true;
4221        }
4222
4223        public function IsHiddenPathAllowed($path)
4224        {
4225                if ($this->options['safe'] && !empty($path))
4226                {
4227                        $path = strtr($path, '\\', '/');
4228                        $segs = explode('/', $path);
4229                        foreach($segs as $file)
4230                        {
4231                                if (!$this->IsHiddenNameAllowed($file))
4232                                {
4233                                        return false;
4234                                }
4235                        }
4236                }
4237                return true;
4238        }
4239
4240
4241        /**
4242         * Make a cleaned-up, unique filename
4243         *
4244         * Return the file (dir + name + ext), or a unique, yet non-existing, variant thereof, where the filename
4245         * is appended with a '_' and a number, e.g. '_1', when the file itself already exists in the given
4246         * directory. The directory part of the returned value equals $dir.
4247         *
4248         * Return NULL when $file is empty or when the specified directory does not reside within the
4249         * directory tree rooted by options['directory']
4250         *
4251         * Note that the given filename will be converted to a legal filename, containing a filesystem-legal
4252         * subset of ASCII characters only, before being used and returned by this function.
4253         *
4254         * @param mixed $fileinfo     either a string containing a filename+ext or an array as produced by pathinfo().
4255         * @daram string $dir         path pointing at where the given file may exist.
4256         *
4257         * @return a filepath consisting of $dir and the cleaned up and possibly sequenced filename and file extension
4258         *         as provided by $fileinfo, or NULL on error.
4259         */
4260        public function getUniqueName($fileinfo, $dir)
4261        {
4262                $dir = self::enforceTrailingSlash($dir);
4263
4264                if (is_string($fileinfo))
4265                {
4266                        $fileinfo = pathinfo($fileinfo);
4267                }
4268
4269                if (!is_array($fileinfo))
4270                {
4271                        return null;
4272                }
4273                $dotfile = (strlen($fileinfo['filename']) == 0);
4274
4275                /*
4276                 * since 'pagetitle()' is used to produce a unique, non-existing filename, we can forego the dirscan
4277                 * and simply check whether the constructed filename/path exists or not and bump the suffix number
4278                 * by 1 until it does not, thus quickly producing a unique filename.
4279                 *
4280                 * This is faster than using a dirscan to collect a set of existing filenames and feeding them as
4281                 * an option array to pagetitle(), particularly for large directories.
4282                 */
4283                $filename = FileManagerUtility::pagetitle($fileinfo['filename'], null, '-_., []()~!@+' /* . '#&' */, '-_,~@+#&');
4284                if (!$filename && !$dotfile)
4285                        return null;
4286
4287                // also clean up the extension: only allow alphanumerics in there!
4288                $ext = FileManagerUtility::pagetitle(isset($fileinfo['extension']) ? $fileinfo['extension'] : null);
4289                $ext = (strlen($ext) > 0 ? '.' . $ext : null);
4290                // make sure the generated filename is SAFE:
4291                $fname = $filename . $ext;
4292                $file = $dir . $fname;
4293                if (file_exists($file))
4294                {
4295                        if ($dotfile)
4296                        {
4297                                $filename = $fname;
4298                                $ext = '';
4299                        }
4300
4301                        /*
4302                         * make a unique name. Do this by postfixing the filename with '_X' where X is a sequential number.
4303                         *
4304                         * Note that when the input name is already such a 'sequenced' name, the sequence number is
4305                         * extracted from it and sequencing continues from there, hence input 'file_5' would, if it already
4306                         * existed, thus be bumped up to become 'file_6' and so on, until a filename is found which
4307                         * does not yet exist in the designated directory.
4308                         */
4309                        $i = 1;
4310                        if (preg_match('/^(.*)_([1-9][0-9]*)$/', $filename, $matches))
4311                        {
4312                                $i = intval($matches[2]);
4313                                if ('P'.$i !== 'P'.$matches[2] || $i > 100000)
4314                                {
4315                                        // very large number: not a sequence number!
4316                                        $i = 1;
4317                                }
4318                                else
4319                                {
4320                                        $filename = $matches[1];
4321                                }
4322                        }
4323                        do
4324                        {
4325                                $fname = $filename . ($i ? '_' . $i : '') . $ext;
4326                                $file = $dir . $fname;
4327                                $i++;
4328                        } while (file_exists($file));
4329                }
4330
4331                // $fname is now guaranteed to NOT exist in the given directory
4332                return $fname;
4333        }
4334
4335
4336
4337        /**
4338         * Predict the actual width/height dimensions of the thumbnail, given the original image's dimensions and the given size limits.
4339         *
4340         * Note: exists as a method in this class, so you can override it when you override getThumb().
4341         */
4342        public function predictThumbDimensions($orig_x, $orig_y, $max_x = null, $max_y = null, $ratio = true, $resizeWhenSmaller = false)
4343        {
4344                return Image::calculate_resize_dimensions($orig_x, $orig_y, $max_x, $max_y, $ratio, $resizeWhenSmaller);
4345        }
4346
4347
4348        /**
4349         * Returns the URI path to the apropriate icon image for the given file / directory.
4350         *
4351         * NOTES:
4352         *
4353         * 1) any $path with an 'extension' of '.directory' is assumed to be a directory.
4354         *
4355         * 2) This method specifically does NOT check whether the given path exists or not: it just looks at
4356         *    the filename extension passed to it, that's all.
4357         *
4358         * Note #2 is important as this enables this function to also serve as icon fetcher for ZIP content viewer, etc.:
4359         * after all, those files do not exist physically on disk themselves!
4360         */
4361        public function getIcon($file, $smallIcon)
4362        {
4363                $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
4364
4365                if (array_key_exists($ext, $this->icon_cache[!$smallIcon]))
4366                {
4367                        return $this->icon_cache[!$smallIcon][$ext];
4368                }
4369
4370                $largeDir = (!$smallIcon ? 'Large/' : '');
4371                $url_path = $this->options['assetBasePath'] . 'Images/Icons/' . $largeDir . $ext . '.png';
4372                $path = (is_file($this->url_path2file_path($url_path)))
4373                        ? $url_path
4374                        : $this->options['assetBasePath'] . 'Images/Icons/' . $largeDir . 'default.png';
4375
4376                $this->icon_cache[!$smallIcon][$ext] = $path;
4377
4378                return $path;
4379        }
4380
4381        /**
4382         * Return the path to the thumbnail of the specified image, the thumbnail having its
4383         * width and height limited to $width pixels.
4384         *
4385         * When the thumbnail image does not exist yet, it will created on the fly.
4386         *
4387         * @param object $meta
4388         *                             the cache record instance related to the original image. Is used
4389         *                             to access the cache and generate a suitable thumbnail filename.
4390         *
4391         * @param string $path         filesystem path to the original image. Is used to derive
4392         *                             the thumbnail content from.
4393         *
4394         * @param integer $width       the maximum number of pixels for width of the
4395         *                             thumbnail.
4396         *
4397         * @param integer $height      the maximum number of pixels for height of the
4398         *                             thumbnail.
4399         */
4400        public function getThumb($meta, $path, $width, $height, $onlyIfExistsInCache = false)
4401        {
4402                $thumbPath = $meta->getThumbPath($width . 'x' . $height);
4403                if (!is_file($thumbPath))
4404                {
4405                        if ($onlyIfExistsInCache)
4406                                return false;
4407
4408                        // make sure the cache subdirectory exists where we are going to store the thumbnail:
4409                        $meta->mkCacheDir();
4410
4411                        $img = new Image($path);
4412                        // generally save as lossy / lower-Q jpeg to reduce filesize, unless orig is PNG/GIF, higher quality for smaller thumbnails:
4413                        $img->resize($width, $height)->save($thumbPath, min(98, max(MTFM_THUMBNAIL_JPEG_QUALITY, MTFM_THUMBNAIL_JPEG_QUALITY + 0.15 * (250 - min($width, $height)))), true);
4414
4415                        if (DEVELOPMENT)
4416                        {
4417                                $imginfo = $img->getMetaInfo();
4418                                $meta->store('img_info', $imginfo);
4419
4420                                $meta->store('memory used', number_format(memory_get_peak_usage() / 1E6, 1) . ' MB');
4421                                $meta->store('memory estimated', number_format(@$imginfo['fileinfo']['usage_guestimate'] / 1E6, 1) . ' MB');
4422                                $meta->store('memory suggested', number_format(@$imginfo['fileinfo']['usage_min_advised'] / 1E6, 1) . ' MB');
4423                        }
4424
4425                        unset($img);
4426                }
4427                return $meta->getThumbURL($width . 'x' . $height);
4428        }
4429
4430        /**
4431         * Assistant function which produces the best possible icon image path for the given error/exception message.
4432         */
4433        public function getIconForError($emsg, $original_filename, $small_icon)
4434        {
4435                if (empty($emsg))
4436                {
4437                        // just go and pick the extension-related icon for this one; nothing is wrong today, it seems.
4438                        $thumb_path = (!empty($original_filename) ? $original_filename : 'is.default-missing');
4439                }
4440                else
4441                {
4442                        $thumb_path = 'is.default-error';
4443
4444                        if (strpos($emsg, 'img_will_not_fit') !== false)
4445                        {
4446                                $thumb_path = 'is.oversized_img';
4447                        }
4448                        else if (strpos($emsg, 'nofile') !== false)
4449                        {
4450                                $thumb_path = 'is.default-missing';
4451                        }
4452                        else if (strpos($emsg, 'unsupported_imgfmt') !== false)
4453                        {
4454                                // just go and pick the extension-related icon for this one; nothing seriously wrong here.
4455                                $thumb_path = (!empty($original_filename) ? $original_filename : $thumb_path);
4456                        }
4457                        else if (strpos($emsg, 'image') !== false)
4458                        {
4459                                $thumb_path = 'badly.broken_img';
4460                        }
4461                }
4462
4463                $img_filepath = $this->getIcon($thumb_path, $small_icon);
4464
4465                return $img_filepath;
4466        }
4467
4468
4469
4470
4471
4472
4473
4474
4475
4476        /**
4477         * Convert the given content to something that is safe to copy straight to HTML
4478         */
4479        public function mkSafe4Display($str)
4480        {
4481                // only allow ASCII to pass:
4482                $str = preg_replace("/[^ -~\t\r\n]/", '?', $str);
4483                $str = str_replace('%3C', '?', $str);             // in case someone want's to get really fancy: nuke the URLencoded '<'
4484
4485                // anything that's more complex than a simple <TAG> is nuked:
4486                $str = preg_replace('/<([\/]?script\s*[\/]?)>/', '?', $str);               // but first make sure '<script>' doesn't make it through the next regex!
4487                $str = preg_replace('/<([\/]?[a-zA-Z]+\s*[\/]?)>/', "\x04\\1\x05", $str);
4488                $str = strip_tags($str);
4489                $str = strtr($str, "\x04", '<');
4490                $str = strtr($str, "\x05", '>');
4491                return $str;
4492        }
4493
4494
4495        /**
4496         * Make data suitable for inclusion in a HTML tag attribute value: strip all tags and encode quotes!
4497         */
4498        public function mkSafe4HTMLattr($str)
4499        {
4500                $str = str_replace('%3C', '?', $str);             // in case someone want's to get really fancy: nuke the URLencoded '<'
4501                $str = strip_tags($str);
4502                return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
4503        }
4504
4505        /**
4506         * inspired by http://nl3.php.net/manual/en/function.utf8-encode.php#102382; mix & mash to make sure the result is LEGAL UTF-8
4507         *
4508         * Introduced after the JSON encoder kept spitting out 'null' instead of a string value for a few choice French JPEGs with very non-UTF EXIF content. :-(
4509         */
4510        public function mkSafeUTF8($str)
4511        {
4512                // kill NUL bytes: they don't belong in here!
4513                $str = strtr($str, "\x00", ' ');
4514
4515                if (!mb_check_encoding($str, 'UTF-8') || $str !== mb_convert_encoding(mb_convert_encoding($str, 'UTF-32', 'UTF-8'), 'UTF-8', 'UTF-32'))
4516                {
4517                        $encoding = mb_detect_encoding($str, 'auto, ISO-8859-1', true);
4518                        $im = str_replace('?', '&qmark;', $str);
4519                        if ($encoding !== false)
4520                        {
4521                                $dst = mb_convert_encoding($im, 'UTF-8', $encoding);
4522                        }
4523                        else
4524                        {
4525                                $dst = mb_convert_encoding($im, 'UTF-8');
4526                        }
4527                        //$dst = utf8_encode($im);
4528                        //$dst = getid3_lib::iconv_fallback('ISO-8859-1', 'UTF-8', $im);
4529
4530                        if (!mb_check_encoding($dst, 'UTF-8') || $dst !== mb_convert_encoding(mb_convert_encoding($dst, 'UTF-32', 'UTF-8'), 'UTF-8', 'UTF-32') || strpos($dst, '?') !== false)
4531                        {
4532                                // not UTF8 yet... try them all
4533                                $encs = mb_list_encodings();
4534                                foreach ($encs as $encoding)
4535                                {
4536                                        $dst = mb_convert_encoding($im, 'UTF-8', $encoding);
4537                                        if (mb_check_encoding($dst, 'UTF-8') && $dst === mb_convert_encoding(mb_convert_encoding($dst, 'UTF-32', 'UTF-8'), 'UTF-8', 'UTF-32') && strpos($dst, '?') === false)
4538                                        {
4539                                                return str_replace('&qmark;', '?', $dst);
4540                                        }
4541                                }
4542
4543                                // when we get here, it's pretty hopeless. Strip ANYTHING that's non-ASCII:
4544                                return preg_replace("/[^ -~\t\r\n]/", '?', $str);
4545                        }
4546
4547                        // UTF8 cannot contain low-ASCII values; at least WE do not allow that!
4548                        if (preg_match("/[^ -\xFF\n\r\t]/", $dst))
4549                        {
4550                                // weird output that's not legible anyhow, so strip ANYTHING that's non-ASCII:
4551                                return preg_replace("/[^ -~\t\r\n]/", '?', $str);
4552                        }
4553                        return str_replace('&qmark;', '?', $dst);
4554                }
4555                return $str;
4556        }
4557
4558
4559
4560        /**
4561         * Safe replacement of dirname(); does not care whether the input has a trailing slash or not.
4562         *
4563         * Return FALSE when the path is attempting to get the parent of '/'
4564         */
4565        public static function getParentDir($path)
4566        {
4567                /*
4568                 * on Windows, you get:
4569                 *
4570                 * dirname("/") = "\"
4571                 * dirname("y/") = "."
4572                 * dirname("/x") = "\"
4573                 *
4574                 * so we'd rather not use dirname()   :-(
4575                 */
4576                if (!is_string($path))
4577                        return false;
4578                $path = rtrim($path, '/');
4579                // 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:
4580                if (strlen($path) <= 1)
4581                        return false;
4582
4583                $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...
4584                if ($p2 === false)
4585                {
4586                        return false; // tampering!
4587                }
4588                $prev = substr($path, 0, $p2 + 1);
4589                return $prev;
4590        }
4591
4592        /**
4593         * Return the URI absolute path to the script pointed at by the current URI request.
4594         * For example, if the request was 'http://site.org/dir1/dir2/script.php', then this method will
4595         * return '/dir1/dir2/script.php'.
4596         *
4597         * By default, this is equivalent to $_SERVER['SCRIPT_NAME'].
4598         *
4599         * This default can be overridden by specifying the options['RequestScriptURI'] when invoking the constructor.
4600         */
4601        public /* static */ function getRequestScriptURI()
4602        {
4603                if (!empty($this->options['RequestScriptURI']))
4604                {
4605                        return $this->options['RequestScriptURI'];
4606                }
4607               
4608                // see also: http://php.about.com/od/learnphp/qt/_SERVER_PHP.htm
4609                $path = strtr($_SERVER['SCRIPT_NAME'], '\\', '/');
4610
4611                return $path;
4612        }
4613
4614        /**
4615         * Return the URI absolute path to the directory pointed at by the current URI request.
4616         * For example, if the request was 'http://site.org/dir1/dir2/script', then this method will
4617         * return '/dir1/dir2/'.
4618         *
4619         * Note that the path is returned WITH a trailing slash '/'.
4620         */
4621        public /* static */ function getRequestPath()
4622        {
4623                $path = self::getParentDir($this->getRequestScriptURI());
4624                $path = self::enforceTrailingSlash($path);
4625
4626                return $path;
4627        }
4628
4629        /**
4630         * Normalize an absolute path by converting all slashes '/' and/or backslashes '\' and any mix thereof in the
4631         * specified path to UNIX/MAC/Win compatible single forward slashes '/'.
4632         *
4633         * Also roll up any ./ and ../ directory elements in there.
4634         *
4635         * Throw an exception when the operation failed to produce a legal path.
4636         */
4637        public /* static */ function normalize($path)
4638        {
4639                $path = preg_replace('/(\\\|\/)+/', '/', $path);
4640
4641                /*
4642                 * fold '../' directory parts to prevent malicious paths such as 'a/../../../../../../../../../etc/'
4643                 * from succeeding
4644                 *
4645                 * to prevent screwups in the folding code, we FIRST clean out the './' directories, to prevent
4646                 * 'a/./.././.././.././.././.././.././.././.././../etc/' from succeeding:
4647                 */
4648                $path = preg_replace('#/(\./)+#', '/', $path);
4649                // special fix: now strip trailing '/.' section; MUST replace by '/' (trailing) or path won't be accepted as legal when this is the '.' requested for root '/'
4650                $path = preg_replace('#/\.$#', '/', $path);
4651
4652                // 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:
4653                $lead = '';
4654                // the leading part may NOT contain any directory separators, as it's for drive letters only.
4655                // So we must check in order to prevent malice like /../../../../../../../c:/dir from making it through.
4656                if (preg_match('#^([A-Za-z]:)?/(.*)$#', $path, $matches))
4657                {
4658                        $lead = $matches[1];
4659                        $path = '/' . $matches[2];
4660                }
4661
4662                while (($pos = strpos($path, '/..')) !== false)
4663                {
4664                        $prev = substr($path, 0, $pos);
4665                        /*
4666                         * on Windows, you get:
4667                         *
4668                         * dirname("/") = "\"
4669                         * dirname("y/") = "."
4670                         * dirname("/x") = "\"
4671                         *
4672                         * so we'd rather not use dirname()   :-(
4673                         */
4674                        $p2 = strrpos($prev, '/');
4675                        if ($p2 === false)
4676                        {
4677                                throw new FileManagerException('path_tampering:' . $path);
4678                        }
4679                        $prev = substr($prev, 0, $p2);
4680                        $next = substr($path, $pos + 3);
4681                        if ($next && $next[0] !== '/')
4682                        {
4683                                throw new FileManagerException('path_tampering:' . $path);
4684                        }
4685                        $path = $prev . $next;
4686                }
4687
4688                $path = $lead . $path;
4689
4690                /*
4691                 * iff there was such a '../../../etc/' attempt, we'll know because there'd be an exception thrown in the loop above.
4692                 */
4693
4694                return $path;
4695        }
4696
4697
4698        /**
4699         * Accept a URI relative or absolute path and transform it to an absolute URI path, i.e. rooted against DocumentRoot.
4700         *
4701         * Relative paths are assumed to be relative to the current request path, i.e. the getRequestPath() produced path.
4702         *
4703         * Note: as it uses normalize(), any illegal path will throw an FileManagerException
4704         *
4705         * Returns a fully normalized URI absolute path.
4706         */
4707        public function rel2abs_url_path($path)
4708        {
4709                $path = strtr($path, '\\', '/');
4710                if (!FileManagerUtility::startsWith($path, '/'))
4711                {
4712                        $based = $this->getRequestPath();
4713                        $path = $based . $path;
4714                }
4715                return $this->normalize($path);
4716        }
4717
4718        /**
4719         * 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'].
4720         *
4721         * Relative paths are assumed to be relative to the current request path, i.e. the getRequestPath() produced path.
4722         *
4723         * Note: as it uses normalize(), any illegal path will throw a FileManagerException
4724         *
4725         * Returns a fully normalized LEGAL URI path.
4726         *
4727         * Throws a FileManagerException when the given path cannot be converted to a LEGAL URL, i.e. when it resides outside the options['directory'] subtree.
4728         */
4729        public function abs2legal_url_path($path)
4730        {
4731                $root = $this->options['directory'];
4732
4733                $path = $this->rel2abs_url_path($path);
4734
4735                // but we MUST make sure the path is still a LEGAL URI, i.e. sitting inside options['directory']:
4736                if (strlen($path) < strlen($root))
4737                        $path = self::enforceTrailingSlash($path);
4738
4739                if (!FileManagerUtility::startsWith($path, $root))
4740                {
4741                        throw new FileManagerException('path_tampering:' . $path);
4742                }
4743
4744                $path = str_replace($root, '/', $path);
4745
4746                return $path;
4747        }
4748
4749        /**
4750         * Accept a relative or absolute LEGAL URI path and transform it to an absolute URI path, i.e. rooted against DocumentRoot.
4751         *
4752         * Relative paths are assumed to be relative to the options['directory'] directory. This makes them equivalent to absolute paths within
4753         * 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
4754         * we wish to resolve here, after all. So, yes, this deviates from the general principle applied elesewhere in the code. :-(
4755         * Nevertheless, it's easier than scanning and tweaking the FM frontend everywhere.
4756         *
4757         * Note: as it uses normalize(), any illegal path will throw a FileManagerException
4758         *
4759         * Returns a fully normalized URI absolute path.
4760         */
4761        public function legal2abs_url_path($path)
4762        {
4763                $path = $this->rel2abs_legal_url_path($path);
4764               
4765                $root = $this->options['directory'];
4766
4767                // clip the trailing '/' off the $root path as $path has a leading '/' already:
4768                $path = substr($root, 0, -1) . $path;
4769
4770                return $path;
4771        }
4772
4773        /**
4774         * Accept a relative or absolute LEGAL URI path and transform it to an absolute LEGAL URI path, i.e. rooted against options['directory'].
4775         *
4776         * Relative paths are assumed to be relative to the options['directory'] directory. This makes them equivalent to absolute paths within
4777         * 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
4778         * we wish to resolve here, after all. So, yes, this deviates from the general principle applied elesewhere in the code. :-(
4779         * Nevertheless, it's easier than scanning and tweaking the FM frontend everywhere.
4780         *
4781         * Note: as it uses normalize(), any illegal path will throw an FileManagerException
4782         *
4783         * Returns a fully normalized LEGAL URI absolute path.
4784         */
4785        public function rel2abs_legal_url_path($path)
4786        {
4787                $path = strtr($path, '\\', '/');
4788                if (!FileManagerUtility::startsWith($path, '/'))
4789                {
4790                        $path = '/' . $path;
4791                }
4792
4793                $path = $this->normalize($path);
4794
4795                return $path;
4796        }
4797
4798        /**
4799         * Return the filesystem absolute path for the relative or absolute URI path.
4800         *
4801         * Note: as it uses normalize(), any illegal path will throw an FileManagerException
4802         *
4803         * Returns a fully normalized filesystem absolute path.
4804         */
4805        public function url_path2file_path($url_path)
4806        {
4807                $url_path = $this->rel2abs_url_path($url_path);
4808
4809                $path = $this->options['documentRootPath'] . $url_path;
4810
4811                return $path;
4812        }
4813
4814        /**
4815         * Return the filesystem absolute path for the relative or absolute LEGAL URI path.
4816         *
4817         * Note: as it uses normalize(), any illegal path will throw an FileManagerException
4818         *
4819         * Returns a fully normalized filesystem absolute path.
4820         */
4821        public function legal_url_path2file_path($url_path)
4822        {
4823                $path = $this->rel2abs_legal_url_path($url_path);
4824
4825                $path = substr($this->managedBaseDir, 0, -1) . $path;
4826
4827                return $path;
4828        }
4829
4830        public static function enforceTrailingSlash($string)
4831        {
4832                return (strrpos($string, '/') === strlen($string) - 1 ? $string : $string . '/');
4833        }
4834
4835
4836
4837
4838
4839        /**
4840         * Produce minimized HTML output; used to cut don't on the content fed
4841         * to JSON_encode() and make it more readable in raw debug view.
4842         */
4843        public static function compressHTML($str)
4844        {
4845                // brute force: replace tabs by spaces and reduce whitespace series to a single space.
4846                //$str = preg_replace('/\s+/', ' ', $str);
4847
4848                return $str;
4849        }
4850
4851
4852        protected /* static */ function modify_json4exception(&$jserr, $emsg, $target_info = null)
4853        {
4854                if (empty($emsg))
4855                        return;
4856
4857                // only set up the new json error report array when this is the first exception we got:
4858                if (empty($jserr['error']))
4859                {
4860                        // check the error message and see if it is a translation code word (with or without parameters) or just a generic error report string
4861                        $e = explode(':', $emsg, 2);
4862                        if (preg_match('/[^A-Za-z0-9_-]/', $e[0]))
4863                        {
4864                                // generic message. ouch.
4865                                $jserr['error'] = $emsg;
4866                        }
4867                        else
4868                        {
4869                                $extra1 = (!empty($e[1]) ? $this->mkSafe4Display($e[1]) : '');
4870                                $extra2 = (!empty($target_info) ? ' (' . $this->mkSafe4Display($target_info) . ')' : '');
4871                                $jserr['error'] = $emsg = '${backend.' . $e[0] . '}';
4872                                if ($e[0] != 'disabled')
4873                                {
4874                                        // only append the extra data when it's NOT the 'disabled on this server' message!
4875                                        $jserr['error'] .=  $extra1 . $extra2;
4876                                }
4877                                else
4878                                {
4879                                        $jserr['error'] .=  ' (${' . $extra1 . '})';
4880                                }
4881                        }
4882                        $jserr['status'] = 0;
4883                }
4884        }
4885
4886
4887
4888
4889
4890
4891        public function getAllowedMimeTypes($mime_filter = null)
4892        {
4893                $mimeTypes = array();
4894
4895                if (empty($mime_filter)) return null;
4896                $mset = explode(',', $mime_filter);
4897                for($i = count($mset) - 1; $i >= 0; $i--)
4898                {
4899                        if (strpos($mset[$i], '/') === false)
4900                                $mset[$i] .= '/';
4901                }
4902
4903                $mimes = $this->getMimeTypeDefinitions();
4904
4905                foreach ($mimes as $k => $mime)
4906                {
4907                        if ($k === '.')
4908                                continue;
4909
4910                        foreach($mset as $filter)
4911                        {
4912                                if (FileManagerUtility::startsWith($mime, $filter))
4913                                        $mimeTypes[] = $mime;
4914                        }
4915                }
4916
4917                return $mimeTypes;
4918        }
4919
4920        public function getMimeTypeDefinitions()
4921        {
4922                static $mimes;
4923
4924                $pref_ext = array();
4925
4926                if (!$mimes)
4927                {
4928                        $mimes = parse_ini_file($this->options['mimeTypesPath']);
4929
4930                        if (is_array($mimes))
4931                        {
4932                                foreach($mimes as $k => $v)
4933                                {
4934                                        $m = explode(',', (string)$v);
4935                                        $mimes[$k] = $m[0];
4936                                        $p = null;
4937                                        if (!empty($m[1]))
4938                                        {
4939                                                $p = trim($m[1]);
4940                                        }
4941                                        // is this the preferred extension for this mime type? Or is this the first known extension for the given mime type?
4942                                        if ($p === '*' || !array_key_exists($m[0], $pref_ext))
4943                                        {
4944                                                $pref_ext[$m[0]] = $k;
4945                                        }
4946                                }
4947
4948                                // stick the mime-to-extension map into an 'illegal' index:
4949                                $mimes['.'] = $pref_ext;
4950                        }
4951                        else
4952                        {
4953                                $mimes = false;
4954                        }
4955                }
4956
4957                if (!is_array($mimes)) $mimes = array(); // prevent faulty mimetype ini file from b0rking other code sections.
4958
4959                return $mimes;
4960        }
4961
4962        public function IsAllowedMimeType($mime_type, $mime_filters)
4963        {
4964                if (empty($mime_type))
4965                        return false;
4966                if (!is_array($mime_filters))
4967                        return true;
4968
4969                return in_array($mime_type, $mime_filters);
4970        }
4971
4972        /**
4973         * Returns (if possible) the mimetype of the given file
4974         *
4975         * @param string $file        physical filesystem path of the file for which we wish to know the mime type.
4976         */
4977        public function getMimeFromExt($file)
4978        {
4979                $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
4980
4981                $mime = null;
4982                if (MTFM_USE_FINFO_OPEN)
4983                {
4984                        $ini = error_reporting(0);
4985                        if (function_exists('finfo_open') && $f = finfo_open(FILEINFO_MIME, getenv('MAGIC')))
4986                        {
4987                                $mime = finfo_file($f, $file);
4988                                // some systems also produce the character encoding with the mime type; strip if off:
4989                                $ma = explode(';', $mime);
4990                                $mime = $ma[0];
4991                                finfo_close($f);
4992                        }
4993                        error_reporting($ini);
4994                }
4995
4996                if ((empty($mime) || $mime === 'application/octet-stream') && strlen($ext) > 0)
4997                {
4998                        $ext2mimetype_arr = $this->getMimeTypeDefinitions();
4999
5000                        if (array_key_exists($ext, $ext2mimetype_arr))
5001                                $mime = $ext2mimetype_arr[$ext];
5002                }
5003
5004                if (empty($mime))
5005                {
5006                        $mime = 'application/octet-stream';
5007                }
5008
5009                return $mime;
5010        }
5011
5012        /**
5013         * Return the first known extension for the given mime type.
5014         *
5015         * Return NULL when no known extension is found.
5016         */
5017        public function getExtFromMime($mime)
5018        {
5019                $ext2mimetype_arr = $this->getMimeTypeDefinitions();
5020                $mime2ext_arr = $ext2mimetype_arr['.'];
5021
5022                if (array_key_exists($mime, $mime2ext_arr))
5023                        return $mime2ext_arr[$mime];
5024
5025                return null;
5026        }
5027
5028        /**
5029         * Returns (if possible) all info about the given file, mimetype, dimensions, the works
5030         *
5031         * @param string $file       physical filesystem path to the file we want to know all about
5032         *
5033         * @param string $legal_url
5034         *                           'legal URL path' to the file; used as the key to the corresponding
5035         *                           cache storage record: getFileInfo() will cache the
5036         *                           extracted info alongside the thumbnails in a cache file with
5037         *                           '.nfo' extension.
5038         *
5039         * @return the info array as produced by getID3::analyze(), as part of a MTFMCacheEntry reference
5040         */
5041        public function getFileInfo($file, $legal_url, $force_recheck = false)
5042        {
5043                // when hash exists in cache, return that one:
5044                $meta = &$this->getid3_cache->pick($legal_url, $this);
5045                assert($meta != null);
5046                $mime_check = $meta->fetch('mime_type');
5047                if (empty($mime_check) || $force_recheck)
5048                {
5049                        // cache entry is not yet filled: we'll have to do the hard work now and store it.
5050                        if (is_dir($file))
5051                        {
5052                                $meta->store('mime_type', 'text/directory', false);
5053                                $meta->store('analysis', null, false);
5054                        }
5055                        else
5056                        {
5057        $rv = $this->analyze_file($file, $legal_url);
5058        $meta->store('mime_type', $rv['mime_type']);       
5059                                $meta->store('analysis', $rv);
5060                        }
5061                }
5062
5063                return $meta;
5064        }
5065
5066
5067
5068
5069
5070        protected /* static */ function getGETparam($name, $default_value = null)
5071        {
5072                if (is_array($_GET) && !empty($_GET[$name]))
5073                {
5074                        $rv = $_GET[$name];
5075
5076                        // see if there's any stuff in there which we don't like
5077                        if (!preg_match('/[^A-Za-z0-9\/~!@#$%^&*()_+{}[]\'",.?]/', $rv))
5078                        {
5079                                return $rv;
5080                        }
5081                }
5082                return $default_value;
5083        }
5084
5085        protected /* static */ function getPOSTparam($name, $default_value = null)
5086        {
5087                if (is_array($_POST) && !empty($_POST[$name]))
5088                {
5089                        $rv = $_POST[$name];
5090
5091                        // see if there's any stuff in there which we don't like
5092                        if (!preg_match('/[^A-Za-z0-9\/~!@#$%^&*()_+{}[]\'",.?]/', $rv))
5093                        {
5094                                return $rv;
5095                        }
5096                }
5097                return $default_value;
5098        }
5099}
5100
5101
5102
5103
5104
5105
5106class FileManagerException extends Exception {}
5107
5108
5109
5110
5111
5112/* Stripped-down version of some Styx PHP Framework-Functionality bundled with this FileBrowser. Styx is located at: http://styx.og5.net */
5113class FileManagerUtility
5114{
5115        public static function endsWith($string, $look)
5116        {
5117                return strrpos($string, $look) === strlen($string) - strlen($look);
5118        }
5119
5120        public static function startsWith($string, $look)
5121        {
5122                return strpos($string, $look) === 0;
5123        }
5124
5125
5126        /**
5127         * Cleanup and check against 'already known names' in optional $options array.
5128         * Return a uniquified name equal to or derived from the original ($data).
5129         *
5130         * First clean up the given name ($data): by default all characters not part of the
5131         * set [A-Za-z0-9_] are converted to an underscore '_'; series of these underscores
5132         * are reduced to a single one, and characters in the set [_.,&+ ] are stripped from
5133         * the lead and tail of the given name, e.g. '__name' would therefor be reduced to
5134         * 'name'.
5135         *
5136         * Next, check the now cleaned-up name $data against an optional set of names ($options array)
5137         * and return the name itself when it does not exist in the set,
5138         * otherwise return an augmented name such that it does not exist in the set
5139         * while having been constructed as name plus '_' plus an integer number,
5140         * starting at 1.
5141         *
5142         * Example:
5143         * If the set is {'file', 'file_1', 'file_3'} then $data = 'file' will return
5144         * the string 'file_2' instead, while $data = 'fileX' will return that same
5145         * value: 'fileX'.
5146         *
5147         * @param string $data     the name to be cleaned and checked/uniquified
5148         * @param array $options   an optional array of strings to check the given name $data against
5149         * @param string $extra_allowed_chars     optional set of additional characters which should pass
5150         *                                        unaltered through the cleanup stage. a dash '-' can be
5151         *                                        used to denote a character range, while the literal
5152         *                                        dash '-' itself, when included, should be positioned
5153         *                                        at the very start or end of the string.
5154         *
5155         *                                        Note that ] must NOT need to be escaped; we do this
5156         *                                        ourselves.
5157         * @param string $trim_chars              optional set of additional characters which are trimmed off the
5158         *                                        start and end of the name ($data); note that de dash
5159         *                                        '-' is always treated as a literal dash here; no
5160         *                                        range feature!
5161         *                                        The basic set of characters trimmed off the name is
5162         *                                        [. ]; this set cannot be reduced, only extended.
5163         *
5164         * @return cleaned-up and uniquified name derived from ($data).
5165         */
5166        public static function pagetitle($data, $options = null, $extra_allowed_chars = null, $trim_chars = null)
5167        {
5168                static $regex;
5169                if (!$regex){
5170                        $regex = array(
5171                                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;'),
5172                                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'),
5173                        );
5174                }
5175
5176                if (empty($data))
5177                {
5178                        return (string)$data;
5179                }
5180
5181                // fixup $extra_allowed_chars to ensure it's suitable as a character sequence for a set in a regex:
5182                //
5183                // Note:
5184                //   caller must ensure a dash '-', when to be treated as a separate character, is at the very end of the string
5185                if (is_string($extra_allowed_chars))
5186                {
5187                        $extra_allowed_chars = str_replace(']', '\]', $extra_allowed_chars);
5188                        if (strpos($extra_allowed_chars, '-') === 0)
5189                        {
5190                                $extra_allowed_chars = substr($extra_allowed_chars, 1) . (strpos($extra_allowed_chars, '-') != strlen($extra_allowed_chars) - 1 ? '-' : '');
5191                        }
5192                }
5193                else
5194                {
5195                        $extra_allowed_chars = '';
5196                }
5197                // 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!
5198                $data = preg_replace('/[^A-Za-z0-9' . $extra_allowed_chars . ']+/', '_', str_replace($regex[0], $regex[1], $data));
5199                $data = trim($data, '_. ' . $trim_chars);
5200
5201                //$data = trim(substr(preg_replace('/(?:[^A-z0-9]|_|\^)+/i', '_', str_replace($regex[0], $regex[1], $data)), 0, 64), '_');
5202                return !empty($options) ? self::checkTitle($data, $options) : $data;
5203        }
5204
5205        protected static function checkTitle($data, $options = array(), $i = 0)
5206        {
5207                if (!is_array($options)) return $data;
5208
5209                $lwr_data = strtolower($data);
5210
5211                foreach ($options as $content)
5212                        if ($content && strtolower($content) == $lwr_data . ($i ? '_' . $i : ''))
5213                                return self::checkTitle($data, $options, ++$i);
5214
5215                return $data.($i ? '_' . $i : '');
5216        }
5217
5218        public static function isBinary($str)
5219        {
5220                for($i = 0; $i < strlen($str); $i++)
5221                {
5222                        $c = ord($str[$i]);
5223                        // do not accept ANY codes below SPACE, except TAB, CR and LF.
5224                        if ($c == 255 || ($c < 32 /* SPACE */ && $c != 9 && $c != 10 && $c != 13)) return true;
5225                }
5226
5227                return false;
5228        }
5229
5230        /**
5231         * Apply rawurlencode() to each of the elements of the given path
5232         *
5233         * @note
5234         *   this method is provided as rawurlencode() itself also encodes the '/' separators in a path/string
5235         *   and we do NOT want to 'revert' such change with the risk of also picking up other %2F bits in
5236         *   the string (this assumes crafted paths can be fed to us).
5237         */
5238        public static function rawurlencode_path($path)
5239        {
5240                return str_replace('%2F', '/', rawurlencode($path));
5241        }
5242
5243        /**
5244         * Convert a number (representing number of bytes) to a formatted string representing GB .. bytes,
5245         * depending on the size of the value.
5246         */
5247        public static function fmt_bytecount($val, $precision = 1)
5248        {
5249                $unit = array('TB', 'GB', 'MB', 'KB', 'bytes');
5250                for ($x = count($unit) - 1; $val >= 1024 && $x > 0; $x--)
5251                {
5252                        $val /= 1024.0;
5253                }
5254                $val = round($val, ($x > 0 ? $precision : 0));
5255                return $val . '&#160;' . $unit[$x];
5256        }
5257
5258
5259
5260
5261        /*
5262         * Derived from getID3 demo_browse.php sample code.
5263         *
5264         * Attempts some 'intelligent' conversions for better readability and information compacting.
5265         */
5266        public static function table_var_dump(&$variable, $wrap_in_td = false, $show_types = false, $level = 0)
5267        {
5268                $returnstring = '';
5269                if (is_array($variable))
5270                {
5271                        $returnstring .= ($wrap_in_td ? '' : '');
5272                        $returnstring .= '<ul class="dump_array dump_level_' . sprintf('%02u', $level) . '">';
5273                        foreach ($variable as $key => &$value)
5274                        {
5275                                // Assign an extra class representing the (rounded) width in number of characters 'or more':
5276                                // You can use this as a width approximation in pixels to style (very) wide items. It saves
5277                                // a long run through all the nodes in JS, just to measure the actual width and correct any
5278                                // overlap occurring in there.
5279                                $keylen = strlen($key);
5280                                $threshold = 10;
5281                                $overlarge_key_class = '';
5282                                while ($keylen >= $threshold)
5283                                {
5284                                        $overlarge_key_class .= ' overlarger' . sprintf('%04d', $threshold);
5285                                        $threshold *= 1.6;
5286                                }
5287
5288                                $returnstring .= '<li><span class="key' . $overlarge_key_class . '">' . $key . '</span>';
5289                                $tstring = '';
5290                                if ($show_types)
5291                                {
5292                                        $tstring = '<span class="type">'.gettype($value);
5293                                        if (is_array($value))
5294                                        {
5295                                                $tstring .= '&nbsp;('.count($value).')';
5296                                        }
5297                                        elseif (is_string($value))
5298                                        {
5299                                                $tstring .= '&nbsp;('.strlen($value).')';
5300                                        }
5301                                        $tstring = '</span>';
5302                                }
5303
5304                                switch ((string)$key)
5305                                {
5306                                case 'filesize':
5307                                        $returnstring .= '<span class="dump_seconds">' . $tstring . self::fmt_bytecount($value) . ($value >= 1024 ? ' (' . $value . ' bytes)' : '') . '</span></li>';
5308                                        continue 2;
5309
5310                                case 'playtime seconds':
5311                                        $returnstring .= '<span class="dump_seconds">' . $tstring . number_format($value, 1) . ' s</span></li>';
5312                                        continue 2;
5313
5314                                case 'compression ratio':
5315                                        $returnstring .= '<span class="dump_compression_ratio">' . $tstring . number_format($value * 100, 1) . '%</span></li>';
5316                                        continue 2;
5317
5318                                case 'bitrate':
5319                                case 'bit rate':
5320                                case 'avg bit rate':
5321                                case 'max bit rate':
5322                                case 'max bitrate':
5323                                case 'sample rate':
5324                                case 'sample rate2':
5325                                case 'samples per sec':
5326                                case 'avg bytes per sec':
5327                                        $returnstring .= '<span class="dump_rate">' . $tstring . self::fmt_bytecount($value) . '/s</span></li>';
5328                                        continue 2;
5329
5330                                case 'bytes per minute':
5331                                        $returnstring .= '<span class="dump_rate">' . $tstring . self::fmt_bytecount($value) . '/min</span></li>';
5332                                        continue 2;
5333                                }
5334                                $returnstring .= FileManagerUtility::table_var_dump($value, true, $show_types, $level + 1) . '</li>';
5335                        }
5336                        $returnstring .= '</ul>';
5337                        $returnstring .= ($wrap_in_td ? '' : '');
5338                }
5339                else if (is_bool($variable))
5340                {
5341                        $returnstring .= ($wrap_in_td ? '<span class="dump_boolean">' : '').($variable ? 'TRUE' : 'FALSE').($wrap_in_td ? '</span>' : '');
5342                }
5343                else if (is_int($variable))
5344                {
5345                        $returnstring .= ($wrap_in_td ? '<span class="dump_integer">' : '').$variable.($wrap_in_td ? '</span>' : '');
5346                }
5347                else if (is_float($variable))
5348                {
5349                        $returnstring .= ($wrap_in_td ? '<span class="dump_double">' : '').$variable.($wrap_in_td ? '</span>' : '');
5350                }
5351                else if (is_object($variable) && isset($variable->id3_procsupport_obj))
5352                {
5353                        if (isset($variable->metadata) && isset($variable->imagedata))
5354                        {
5355                                // an embedded image (MP3 et al)
5356                                $returnstring .= ($wrap_in_td ? '<div class="dump_embedded_image">' : '');
5357                                $returnstring .= '<table class="dump_image">';
5358                                $returnstring .= '<tr><td><b>type</b></td><td>'.getid3_lib::ImageTypesLookup($variable->metadata[2]).'</td></tr>';
5359                                $returnstring .= '<tr><td><b>width</b></td><td>'.number_format($variable->metadata[0]).' px</td></tr>';
5360                                $returnstring .= '<tr><td><b>height</b></td><td>'.number_format($variable->metadata[1]).' px</td></tr>';
5361                                $returnstring .= '<tr><td><b>size</b></td><td>'.number_format(strlen($variable->imagedata)).' bytes</td></tr></table>';
5362                                $returnstring .= '<img src="data:'.$variable->metadata['mime'].';base64,'.base64_encode($variable->imagedata).'" width="'.$variable->metadata[0].'" height="'.$variable->metadata[1].'">';
5363                                $returnstring .= ($wrap_in_td ? '</div>' : '');
5364                        }
5365                        else if (isset($variable->binarydata_mode))
5366                        {
5367                                $returnstring .= ($wrap_in_td ? '<span class="dump_binary_data">' : '');
5368                                if ($variable->binarydata_mode == 'procd')
5369                                {
5370                                        $returnstring .= '<i>' . self::table_var_dump($variable->binarydata, false, false, $level + 1) . '</i>';
5371                                }
5372                                else
5373                                {
5374                                        $temp = unpack('H*', $variable->binarydata);
5375                                        $temp = str_split($temp[1], 8);
5376                                        $returnstring .= '<i>' . self::table_var_dump(implode(' ', $temp), false, false, $level + 1) . '</i>';
5377                                }
5378                                $returnstring .= ($wrap_in_td ? '</span>' : '');
5379                        }
5380                        else
5381                        {
5382                                $returnstring .= ($wrap_in_td ? '<span class="dump_object">' : '').print_r($variable, true).($wrap_in_td ? '</span>' : '');
5383                        }
5384                }
5385                else if (is_object($variable))
5386                {
5387                        $returnstring .= ($wrap_in_td ? '<span class="dump_object">' : '').print_r($variable, true).($wrap_in_td ? '</span>' : '');
5388                }
5389                else if (is_null($variable))
5390                {
5391                        $returnstring .= ($wrap_in_td ? '<span class="dump_null">' : '').'(null)'.($wrap_in_td ? '</span>' : '');
5392                }
5393                else if (is_string($variable))
5394                {
5395                        $variable = strtr($variable, "\x00", ' ');
5396                        $varlen = strlen($variable);
5397                        for ($i = 0; $i < $varlen; $i++)
5398                        {
5399                                $returnstring .= htmlentities($variable{$i}, ENT_QUOTES, 'UTF-8');
5400                        }
5401                        $returnstring = ($wrap_in_td ? '<span class="dump_string">' : '').nl2br($returnstring).($wrap_in_td ? '</span>' : '');
5402                }
5403                else
5404                {
5405                        $returnstring .= ($wrap_in_td ? '<span class="dump_other">' : '').nl2br(htmlspecialchars(strtr($variable, "\x00", ' '))).($wrap_in_td ? '</span>' : '');
5406                }
5407                return $returnstring;
5408        }
5409}
5410
5411
5412
5413// support class for the getID3 info and embedded image extraction:
5414class EmbeddedImageContainer
5415{
5416        public $metadata;
5417        public $imagedata;
5418        public $id3_procsupport_obj;
5419
5420        public function __construct($meta, $img)
5421        {
5422                $this->metadata = $meta;
5423                $this->imagedata = $img;
5424                $this->id3_procsupport_obj = true;
5425        }
5426
5427        public static function __set_state($arr)
5428        {
5429                $obj = new EmbeddedImageContainer($arr['metadata'], $arr['imagedata']);
5430                return $obj;
5431        }
5432}
5433
5434class BinaryDataContainer
5435{
5436        public $binarydata;
5437        public $binarydata_mode;
5438        public $id3_procsupport_obj;
5439
5440        public function __construct($data, $mode = 'procd')
5441        {
5442                $this->binarydata_mode = $mode;
5443                $this->binarydata = $data;
5444                $this->id3_procsupport_obj = true;
5445        }
5446
5447        public static function __set_state($arr)
5448        {
5449                $obj = new BinaryDataContainer($arr['binarydata'], $arr['binarydata_mode']);
5450                return $obj;
5451        }
5452}
5453
Note: See TracBrowser for help on using the repository browser.