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

Last change on this file since 1371 was 1371, checked in by gogo, 21 months ago

Updates to MootoolsFileManager? - most importantly removal of Flash dependancy, now uses HTML5 file uploads.

Also now updated the default MooTools? to 1.6.0 - you can of course load your own MooTools? first (before Xinha), anything down to about 1.3 works I think, if you load yours then it will be used instead.

Other updates from https://github.com/sleemanj/mootools-filemanager included in this update are:

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