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

Last change on this file since 1309 was 1309, checked in by gogo, 8 years ago

Part 2: Add new mootools-filemanager

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