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

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

Updates to http://www.github.com/sleemanj/mootools-filemanager from GerHoblett?

http://www.github.com/GerHoblett/

Changes to said updates by gogo (sleemanj @ github)

Modifications to MootoolsFileManager? to work with those updates, some courtesy of GerHoblett?, some sleemanj

GerHoblett? provided a large diff which accomplished the goal in a quite different way. It has merit, however I have opted for a less-affecting path in so far as Xinha's "way" is concerned, namely, not splitting the config for a single plugin into several calls to backend config passing functions which seemed a little cumbersome.

Instead I take the option of using POST to send backend data around, at the minor expense of an extra round trip when displaying thumbnails (for each one). This could be reduced by checking for thumbnail existence and returning the thumbnail name directly in "onView" rather than the backend request to generate said thumbnail.

Still to do, is to make the preview pane thumbnail also work.


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