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

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

Reduce roundtrips for image thumbnails.
See:

https://github.com/sleemanj/mootools-filemanager/commit/b39b68cda2d7c9f06cdaaeb2aa6556965ae99604

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