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

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

Merge the MootoolsFileManager?-Update branch into the trunk.

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