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

Last change on this file since 1303 was 1303, checked in by gogo, 9 years ago

Make preview images work with our POST type propagateData.
See also

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

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