1
|
<?php
|
2
|
|
3
|
/**
|
4
|
* @file
|
5
|
* API for handling file uploads and server file management.
|
6
|
*/
|
7
|
|
8
|
/**
|
9
|
* Manually include stream wrapper code.
|
10
|
*
|
11
|
* Stream wrapper code is included here because there are cases where
|
12
|
* File API is needed before a bootstrap, or in an alternate order (e.g.
|
13
|
* maintenance theme).
|
14
|
*/
|
15
|
require_once DRUPAL_ROOT . '/includes/stream_wrappers.inc';
|
16
|
|
17
|
/**
|
18
|
* @defgroup file File interface
|
19
|
* @{
|
20
|
* Common file handling functions.
|
21
|
*
|
22
|
* Fields on the file object:
|
23
|
* - fid: File ID
|
24
|
* - uid: The {users}.uid of the user who is associated with the file.
|
25
|
* - filename: Name of the file with no path components. This may differ from
|
26
|
* the basename of the filepath if the file is renamed to avoid overwriting
|
27
|
* an existing file.
|
28
|
* - uri: URI of the file.
|
29
|
* - filemime: The file's MIME type.
|
30
|
* - filesize: The size of the file in bytes.
|
31
|
* - status: A bitmapped field indicating the status of the file. The first 8
|
32
|
* bits are reserved for Drupal core. The least significant bit indicates
|
33
|
* temporary (0) or permanent (1). Temporary files older than
|
34
|
* DRUPAL_MAXIMUM_TEMP_FILE_AGE will be removed during cron runs.
|
35
|
* - timestamp: UNIX timestamp for the date the file was added to the database.
|
36
|
*/
|
37
|
|
38
|
/**
|
39
|
* Flag used by file_prepare_directory() -- create directory if not present.
|
40
|
*/
|
41
|
define('FILE_CREATE_DIRECTORY', 1);
|
42
|
|
43
|
/**
|
44
|
* Flag used by file_prepare_directory() -- file permissions may be changed.
|
45
|
*/
|
46
|
define('FILE_MODIFY_PERMISSIONS', 2);
|
47
|
|
48
|
/**
|
49
|
* Flag for dealing with existing files: Appends number until name is unique.
|
50
|
*/
|
51
|
define('FILE_EXISTS_RENAME', 0);
|
52
|
|
53
|
/**
|
54
|
* Flag for dealing with existing files: Replace the existing file.
|
55
|
*/
|
56
|
define('FILE_EXISTS_REPLACE', 1);
|
57
|
|
58
|
/**
|
59
|
* Flag for dealing with existing files: Do nothing and return FALSE.
|
60
|
*/
|
61
|
define('FILE_EXISTS_ERROR', 2);
|
62
|
|
63
|
/**
|
64
|
* Indicates that the file is permanent and should not be deleted.
|
65
|
*
|
66
|
* Temporary files older than DRUPAL_MAXIMUM_TEMP_FILE_AGE will be removed
|
67
|
* during cron runs, but permanent files will not be removed during the file
|
68
|
* garbage collection process.
|
69
|
*/
|
70
|
define('FILE_STATUS_PERMANENT', 1);
|
71
|
|
72
|
/**
|
73
|
* Provides Drupal stream wrapper registry.
|
74
|
*
|
75
|
* A stream wrapper is an abstraction of a file system that allows Drupal to
|
76
|
* use the same set of methods to access both local files and remote resources.
|
77
|
*
|
78
|
* Provide a facility for managing and querying user-defined stream wrappers
|
79
|
* in PHP. PHP's internal stream_get_wrappers() doesn't return the class
|
80
|
* registered to handle a stream, which we need to be able to find the handler
|
81
|
* for class instantiation.
|
82
|
*
|
83
|
* If a module registers a scheme that is already registered with PHP, the
|
84
|
* existing scheme will be unregistered and replaced with the specified class.
|
85
|
*
|
86
|
* A stream is referenced as "scheme://target".
|
87
|
*
|
88
|
* The optional $filter parameter can be used to retrieve only the stream
|
89
|
* wrappers that are appropriate for particular usage. For example, this returns
|
90
|
* only stream wrappers that use local file storage:
|
91
|
* @code
|
92
|
* $local_stream_wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL);
|
93
|
* @endcode
|
94
|
*
|
95
|
* The $filter parameter can only filter to types containing a particular flag.
|
96
|
* In some cases, you may want to filter to types that do not contain a
|
97
|
* particular flag. For example, you may want to retrieve all stream wrappers
|
98
|
* that are not writable, or all stream wrappers that are not local. PHP's
|
99
|
* array_diff_key() function can be used to help with this. For example, this
|
100
|
* returns only stream wrappers that do not use local file storage:
|
101
|
* @code
|
102
|
* $remote_stream_wrappers = array_diff_key(file_get_stream_wrappers(STREAM_WRAPPERS_ALL), file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL));
|
103
|
* @endcode
|
104
|
*
|
105
|
* @param $filter
|
106
|
* (Optional) Filters out all types except those with an on bit for each on
|
107
|
* bit in $filter. For example, if $filter is STREAM_WRAPPERS_WRITE_VISIBLE,
|
108
|
* which is equal to (STREAM_WRAPPERS_READ | STREAM_WRAPPERS_WRITE |
|
109
|
* STREAM_WRAPPERS_VISIBLE), then only stream wrappers with all three of these
|
110
|
* bits set are returned. Defaults to STREAM_WRAPPERS_ALL, which returns all
|
111
|
* registered stream wrappers.
|
112
|
*
|
113
|
* @return
|
114
|
* An array keyed by scheme, with values containing an array of information
|
115
|
* about the stream wrapper, as returned by hook_stream_wrappers(). If $filter
|
116
|
* is omitted or set to STREAM_WRAPPERS_ALL, the entire Drupal stream wrapper
|
117
|
* registry is returned. Otherwise only the stream wrappers whose 'type'
|
118
|
* bitmask has an on bit for each bit specified in $filter are returned.
|
119
|
*
|
120
|
* @see hook_stream_wrappers()
|
121
|
* @see hook_stream_wrappers_alter()
|
122
|
*/
|
123
|
function file_get_stream_wrappers($filter = STREAM_WRAPPERS_ALL) {
|
124
|
$wrappers_storage = &drupal_static(__FUNCTION__);
|
125
|
|
126
|
if (!isset($wrappers_storage)) {
|
127
|
$wrappers = module_invoke_all('stream_wrappers');
|
128
|
foreach ($wrappers as $scheme => $info) {
|
129
|
// Add defaults.
|
130
|
$wrappers[$scheme] += array('type' => STREAM_WRAPPERS_NORMAL);
|
131
|
}
|
132
|
drupal_alter('stream_wrappers', $wrappers);
|
133
|
$existing = stream_get_wrappers();
|
134
|
foreach ($wrappers as $scheme => $info) {
|
135
|
// We only register classes that implement our interface.
|
136
|
if (in_array('DrupalStreamWrapperInterface', class_implements($info['class']), TRUE)) {
|
137
|
// Record whether we are overriding an existing scheme.
|
138
|
if (in_array($scheme, $existing, TRUE)) {
|
139
|
$wrappers[$scheme]['override'] = TRUE;
|
140
|
stream_wrapper_unregister($scheme);
|
141
|
}
|
142
|
else {
|
143
|
$wrappers[$scheme]['override'] = FALSE;
|
144
|
}
|
145
|
if (($info['type'] & STREAM_WRAPPERS_LOCAL) == STREAM_WRAPPERS_LOCAL) {
|
146
|
stream_wrapper_register($scheme, $info['class']);
|
147
|
}
|
148
|
else {
|
149
|
stream_wrapper_register($scheme, $info['class'], STREAM_IS_URL);
|
150
|
}
|
151
|
}
|
152
|
// Pre-populate the static cache with the filters most typically used.
|
153
|
$wrappers_storage[STREAM_WRAPPERS_ALL][$scheme] = $wrappers[$scheme];
|
154
|
if (($info['type'] & STREAM_WRAPPERS_WRITE_VISIBLE) == STREAM_WRAPPERS_WRITE_VISIBLE) {
|
155
|
$wrappers_storage[STREAM_WRAPPERS_WRITE_VISIBLE][$scheme] = $wrappers[$scheme];
|
156
|
}
|
157
|
}
|
158
|
}
|
159
|
|
160
|
if (!isset($wrappers_storage[$filter])) {
|
161
|
$wrappers_storage[$filter] = array();
|
162
|
foreach ($wrappers_storage[STREAM_WRAPPERS_ALL] as $scheme => $info) {
|
163
|
// Bit-wise filter.
|
164
|
if (($info['type'] & $filter) == $filter) {
|
165
|
$wrappers_storage[$filter][$scheme] = $info;
|
166
|
}
|
167
|
}
|
168
|
}
|
169
|
|
170
|
return $wrappers_storage[$filter];
|
171
|
}
|
172
|
|
173
|
/**
|
174
|
* Returns the stream wrapper class name for a given scheme.
|
175
|
*
|
176
|
* @param $scheme
|
177
|
* Stream scheme.
|
178
|
*
|
179
|
* @return
|
180
|
* Return string if a scheme has a registered handler, or FALSE.
|
181
|
*/
|
182
|
function file_stream_wrapper_get_class($scheme) {
|
183
|
$wrappers = file_get_stream_wrappers();
|
184
|
return empty($wrappers[$scheme]) ? FALSE : $wrappers[$scheme]['class'];
|
185
|
}
|
186
|
|
187
|
/**
|
188
|
* Returns the scheme of a URI (e.g. a stream).
|
189
|
*
|
190
|
* @param $uri
|
191
|
* A stream, referenced as "scheme://target".
|
192
|
*
|
193
|
* @return
|
194
|
* A string containing the name of the scheme, or FALSE if none. For example,
|
195
|
* the URI "public://example.txt" would return "public".
|
196
|
*
|
197
|
* @see file_uri_target()
|
198
|
*/
|
199
|
function file_uri_scheme($uri) {
|
200
|
$position = strpos($uri, '://');
|
201
|
return $position ? substr($uri, 0, $position) : FALSE;
|
202
|
}
|
203
|
|
204
|
/**
|
205
|
* Checks that the scheme of a stream URI is valid.
|
206
|
*
|
207
|
* Confirms that there is a registered stream handler for the provided scheme
|
208
|
* and that it is callable. This is useful if you want to confirm a valid
|
209
|
* scheme without creating a new instance of the registered handler.
|
210
|
*
|
211
|
* @param $scheme
|
212
|
* A URI scheme, a stream is referenced as "scheme://target".
|
213
|
*
|
214
|
* @return
|
215
|
* Returns TRUE if the string is the name of a validated stream,
|
216
|
* or FALSE if the scheme does not have a registered handler.
|
217
|
*/
|
218
|
function file_stream_wrapper_valid_scheme($scheme) {
|
219
|
// Does the scheme have a registered handler that is callable?
|
220
|
$class = file_stream_wrapper_get_class($scheme);
|
221
|
if (class_exists($class)) {
|
222
|
return TRUE;
|
223
|
}
|
224
|
else {
|
225
|
return FALSE;
|
226
|
}
|
227
|
}
|
228
|
|
229
|
|
230
|
/**
|
231
|
* Returns the part of a URI after the schema.
|
232
|
*
|
233
|
* @param $uri
|
234
|
* A stream, referenced as "scheme://target".
|
235
|
*
|
236
|
* @return
|
237
|
* A string containing the target (path), or FALSE if none.
|
238
|
* For example, the URI "public://sample/test.txt" would return
|
239
|
* "sample/test.txt".
|
240
|
*
|
241
|
* @see file_uri_scheme()
|
242
|
*/
|
243
|
function file_uri_target($uri) {
|
244
|
$data = explode('://', $uri, 2);
|
245
|
|
246
|
// Remove erroneous leading or trailing, forward-slashes and backslashes.
|
247
|
return count($data) == 2 ? trim($data[1], '\/') : FALSE;
|
248
|
}
|
249
|
|
250
|
/**
|
251
|
* Gets the default file stream implementation.
|
252
|
*
|
253
|
* @return
|
254
|
* 'public', 'private' or any other file scheme defined as the default.
|
255
|
*/
|
256
|
function file_default_scheme() {
|
257
|
return variable_get('file_default_scheme', 'public');
|
258
|
}
|
259
|
|
260
|
/**
|
261
|
* Normalizes a URI by making it syntactically correct.
|
262
|
*
|
263
|
* A stream is referenced as "scheme://target".
|
264
|
*
|
265
|
* The following actions are taken:
|
266
|
* - Remove trailing slashes from target
|
267
|
* - Trim erroneous leading slashes from target. e.g. ":///" becomes "://".
|
268
|
*
|
269
|
* @param $uri
|
270
|
* String reference containing the URI to normalize.
|
271
|
*
|
272
|
* @return
|
273
|
* The normalized URI.
|
274
|
*/
|
275
|
function file_stream_wrapper_uri_normalize($uri) {
|
276
|
$scheme = file_uri_scheme($uri);
|
277
|
|
278
|
if ($scheme && file_stream_wrapper_valid_scheme($scheme)) {
|
279
|
$target = file_uri_target($uri);
|
280
|
|
281
|
if ($target !== FALSE) {
|
282
|
$uri = $scheme . '://' . $target;
|
283
|
}
|
284
|
}
|
285
|
return $uri;
|
286
|
}
|
287
|
|
288
|
/**
|
289
|
* Returns a reference to the stream wrapper class responsible for a given URI.
|
290
|
*
|
291
|
* The scheme determines the stream wrapper class that should be
|
292
|
* used by consulting the stream wrapper registry.
|
293
|
*
|
294
|
* @param $uri
|
295
|
* A stream, referenced as "scheme://target".
|
296
|
*
|
297
|
* @return
|
298
|
* Returns a new stream wrapper object appropriate for the given URI or FALSE
|
299
|
* if no registered handler could be found. For example, a URI of
|
300
|
* "private://example.txt" would return a new private stream wrapper object
|
301
|
* (DrupalPrivateStreamWrapper).
|
302
|
*/
|
303
|
function file_stream_wrapper_get_instance_by_uri($uri) {
|
304
|
$scheme = file_uri_scheme($uri);
|
305
|
$class = file_stream_wrapper_get_class($scheme);
|
306
|
if (class_exists($class)) {
|
307
|
$instance = new $class();
|
308
|
$instance->setUri($uri);
|
309
|
return $instance;
|
310
|
}
|
311
|
else {
|
312
|
return FALSE;
|
313
|
}
|
314
|
}
|
315
|
|
316
|
/**
|
317
|
* Returns a reference to the stream wrapper class responsible for a scheme.
|
318
|
*
|
319
|
* This helper method returns a stream instance using a scheme. That is, the
|
320
|
* passed string does not contain a "://". For example, "public" is a scheme
|
321
|
* but "public://" is a URI (stream). This is because the later contains both
|
322
|
* a scheme and target despite target being empty.
|
323
|
*
|
324
|
* Note: the instance URI will be initialized to "scheme://" so that you can
|
325
|
* make the customary method calls as if you had retrieved an instance by URI.
|
326
|
*
|
327
|
* @param $scheme
|
328
|
* If the stream was "public://target", "public" would be the scheme.
|
329
|
*
|
330
|
* @return
|
331
|
* Returns a new stream wrapper object appropriate for the given $scheme.
|
332
|
* For example, for the public scheme a stream wrapper object
|
333
|
* (DrupalPublicStreamWrapper).
|
334
|
* FALSE is returned if no registered handler could be found.
|
335
|
*/
|
336
|
function file_stream_wrapper_get_instance_by_scheme($scheme) {
|
337
|
$class = file_stream_wrapper_get_class($scheme);
|
338
|
if (class_exists($class)) {
|
339
|
$instance = new $class();
|
340
|
$instance->setUri($scheme . '://');
|
341
|
return $instance;
|
342
|
}
|
343
|
else {
|
344
|
return FALSE;
|
345
|
}
|
346
|
}
|
347
|
|
348
|
/**
|
349
|
* Creates a web-accessible URL for a stream to an external or local file.
|
350
|
*
|
351
|
* Compatibility: normal paths and stream wrappers.
|
352
|
*
|
353
|
* There are two kinds of local files:
|
354
|
* - "managed files", i.e. those stored by a Drupal-compatible stream wrapper.
|
355
|
* These are files that have either been uploaded by users or were generated
|
356
|
* automatically (for example through CSS aggregation).
|
357
|
* - "shipped files", i.e. those outside of the files directory, which ship as
|
358
|
* part of Drupal core or contributed modules or themes.
|
359
|
*
|
360
|
* @param $uri
|
361
|
* The URI to a file for which we need an external URL, or the path to a
|
362
|
* shipped file.
|
363
|
*
|
364
|
* @return
|
365
|
* A string containing a URL that may be used to access the file.
|
366
|
* If the provided string already contains a preceding 'http', 'https', or
|
367
|
* '/', nothing is done and the same string is returned. If a stream wrapper
|
368
|
* could not be found to generate an external URL, then FALSE is returned.
|
369
|
*
|
370
|
* @see http://drupal.org/node/515192
|
371
|
*/
|
372
|
function file_create_url($uri) {
|
373
|
// Allow the URI to be altered, e.g. to serve a file from a CDN or static
|
374
|
// file server.
|
375
|
drupal_alter('file_url', $uri);
|
376
|
|
377
|
$scheme = file_uri_scheme($uri);
|
378
|
|
379
|
if (!$scheme) {
|
380
|
// Allow for:
|
381
|
// - root-relative URIs (e.g. /foo.jpg in http://example.com/foo.jpg)
|
382
|
// - protocol-relative URIs (e.g. //bar.jpg, which is expanded to
|
383
|
// http://example.com/bar.jpg by the browser when viewing a page over
|
384
|
// HTTP and to https://example.com/bar.jpg when viewing a HTTPS page)
|
385
|
// Both types of relative URIs are characterized by a leading slash, hence
|
386
|
// we can use a single check.
|
387
|
if (drupal_substr($uri, 0, 1) == '/') {
|
388
|
return $uri;
|
389
|
}
|
390
|
else {
|
391
|
// If this is not a properly formatted stream, then it is a shipped file.
|
392
|
// Therefore, return the urlencoded URI with the base URL prepended.
|
393
|
return $GLOBALS['base_url'] . '/' . drupal_encode_path($uri);
|
394
|
}
|
395
|
}
|
396
|
elseif ($scheme == 'http' || $scheme == 'https') {
|
397
|
// Check for HTTP so that we don't have to implement getExternalUrl() for
|
398
|
// the HTTP wrapper.
|
399
|
return $uri;
|
400
|
}
|
401
|
else {
|
402
|
// Attempt to return an external URL using the appropriate wrapper.
|
403
|
if ($wrapper = file_stream_wrapper_get_instance_by_uri($uri)) {
|
404
|
return $wrapper->getExternalUrl();
|
405
|
}
|
406
|
else {
|
407
|
return FALSE;
|
408
|
}
|
409
|
}
|
410
|
}
|
411
|
|
412
|
/**
|
413
|
* Checks that the directory exists and is writable.
|
414
|
*
|
415
|
* Directories need to have execute permissions to be considered a directory by
|
416
|
* FTP servers, etc.
|
417
|
*
|
418
|
* @param $directory
|
419
|
* A string reference containing the name of a directory path or URI. A
|
420
|
* trailing slash will be trimmed from a path.
|
421
|
* @param $options
|
422
|
* A bitmask to indicate if the directory should be created if it does
|
423
|
* not exist (FILE_CREATE_DIRECTORY) or made writable if it is read-only
|
424
|
* (FILE_MODIFY_PERMISSIONS).
|
425
|
*
|
426
|
* @return
|
427
|
* TRUE if the directory exists (or was created) and is writable. FALSE
|
428
|
* otherwise.
|
429
|
*/
|
430
|
function file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS) {
|
431
|
if (!file_stream_wrapper_valid_scheme(file_uri_scheme($directory))) {
|
432
|
// Only trim if we're not dealing with a stream.
|
433
|
$directory = rtrim($directory, '/\\');
|
434
|
}
|
435
|
|
436
|
// Check if directory exists.
|
437
|
if (!is_dir($directory)) {
|
438
|
// Let mkdir() recursively create directories and use the default directory
|
439
|
// permissions.
|
440
|
if (($options & FILE_CREATE_DIRECTORY) && @drupal_mkdir($directory, NULL, TRUE)) {
|
441
|
return drupal_chmod($directory);
|
442
|
}
|
443
|
return FALSE;
|
444
|
}
|
445
|
// The directory exists, so check to see if it is writable.
|
446
|
$writable = is_writable($directory);
|
447
|
if (!$writable && ($options & FILE_MODIFY_PERMISSIONS)) {
|
448
|
return drupal_chmod($directory);
|
449
|
}
|
450
|
|
451
|
return $writable;
|
452
|
}
|
453
|
|
454
|
/**
|
455
|
* Creates a .htaccess file in each Drupal files directory if it is missing.
|
456
|
*/
|
457
|
function file_ensure_htaccess() {
|
458
|
file_create_htaccess('public://', FALSE);
|
459
|
if (variable_get('file_private_path', FALSE)) {
|
460
|
file_create_htaccess('private://', TRUE);
|
461
|
}
|
462
|
file_create_htaccess('temporary://', TRUE);
|
463
|
}
|
464
|
|
465
|
/**
|
466
|
* Creates a .htaccess file in the given directory.
|
467
|
*
|
468
|
* @param $directory
|
469
|
* The directory.
|
470
|
* @param $private
|
471
|
* FALSE indicates that $directory should be an open and public directory.
|
472
|
* The default is TRUE which indicates a private and protected directory.
|
473
|
* @param $force_overwrite
|
474
|
* Set to TRUE to attempt to overwrite the existing .htaccess file if one is
|
475
|
* already present. Defaults to FALSE.
|
476
|
*/
|
477
|
function file_create_htaccess($directory, $private = TRUE, $force_overwrite = FALSE) {
|
478
|
if (file_uri_scheme($directory)) {
|
479
|
$directory = file_stream_wrapper_uri_normalize($directory);
|
480
|
}
|
481
|
else {
|
482
|
$directory = rtrim($directory, '/\\');
|
483
|
}
|
484
|
$htaccess_path = $directory . '/.htaccess';
|
485
|
|
486
|
if (file_exists($htaccess_path) && !$force_overwrite) {
|
487
|
// Short circuit if the .htaccess file already exists.
|
488
|
return;
|
489
|
}
|
490
|
|
491
|
$htaccess_lines = file_htaccess_lines($private);
|
492
|
|
493
|
// Write the .htaccess file.
|
494
|
if (file_put_contents($htaccess_path, $htaccess_lines)) {
|
495
|
drupal_chmod($htaccess_path, 0444);
|
496
|
}
|
497
|
else {
|
498
|
$variables = array('%directory' => $directory, '!htaccess' => '<br />' . nl2br(check_plain($htaccess_lines)));
|
499
|
watchdog('security', "Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: <code>!htaccess</code>", $variables, WATCHDOG_ERROR);
|
500
|
}
|
501
|
}
|
502
|
|
503
|
/**
|
504
|
* Returns the standard .htaccess lines that Drupal writes to file directories.
|
505
|
*
|
506
|
* @param $private
|
507
|
* (Optional) Set to FALSE to return the .htaccess lines for an open and
|
508
|
* public directory. The default is TRUE, which returns the .htaccess lines
|
509
|
* for a private and protected directory.
|
510
|
*
|
511
|
* @return
|
512
|
* A string representing the desired contents of the .htaccess file.
|
513
|
*
|
514
|
* @see file_create_htaccess()
|
515
|
*/
|
516
|
function file_htaccess_lines($private = TRUE) {
|
517
|
$lines = <<<EOF
|
518
|
# Turn off all options we don't need.
|
519
|
Options None
|
520
|
Options +FollowSymLinks
|
521
|
|
522
|
# Set the catch-all handler to prevent scripts from being executed.
|
523
|
SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006
|
524
|
<Files *>
|
525
|
# Override the handler again if we're run later in the evaluation list.
|
526
|
SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003
|
527
|
</Files>
|
528
|
|
529
|
# If we know how to do it safely, disable the PHP engine entirely.
|
530
|
<IfModule mod_php5.c>
|
531
|
php_flag engine off
|
532
|
</IfModule>
|
533
|
EOF;
|
534
|
|
535
|
if ($private) {
|
536
|
$lines = "Deny from all\n\n" . $lines;
|
537
|
}
|
538
|
|
539
|
return $lines;
|
540
|
}
|
541
|
|
542
|
/**
|
543
|
* Loads file objects from the database.
|
544
|
*
|
545
|
* @param $fids
|
546
|
* An array of file IDs.
|
547
|
* @param $conditions
|
548
|
* (deprecated) An associative array of conditions on the {file_managed}
|
549
|
* table, where the keys are the database fields and the values are the
|
550
|
* values those fields must have. Instead, it is preferable to use
|
551
|
* EntityFieldQuery to retrieve a list of entity IDs loadable by
|
552
|
* this function.
|
553
|
*
|
554
|
* @return
|
555
|
* An array of file objects, indexed by fid.
|
556
|
*
|
557
|
* @todo Remove $conditions in Drupal 8.
|
558
|
*
|
559
|
* @see hook_file_load()
|
560
|
* @see file_load()
|
561
|
* @see entity_load()
|
562
|
* @see EntityFieldQuery
|
563
|
*/
|
564
|
function file_load_multiple($fids = array(), $conditions = array()) {
|
565
|
return entity_load('file', $fids, $conditions);
|
566
|
}
|
567
|
|
568
|
/**
|
569
|
* Loads a single file object from the database.
|
570
|
*
|
571
|
* @param $fid
|
572
|
* A file ID.
|
573
|
*
|
574
|
* @return
|
575
|
* An object representing the file, or FALSE if the file was not found.
|
576
|
*
|
577
|
* @see hook_file_load()
|
578
|
* @see file_load_multiple()
|
579
|
*/
|
580
|
function file_load($fid) {
|
581
|
$files = file_load_multiple(array($fid), array());
|
582
|
return reset($files);
|
583
|
}
|
584
|
|
585
|
/**
|
586
|
* Saves a file object to the database.
|
587
|
*
|
588
|
* If the $file->fid is not set a new record will be added.
|
589
|
*
|
590
|
* @param $file
|
591
|
* A file object returned by file_load().
|
592
|
*
|
593
|
* @return
|
594
|
* The updated file object.
|
595
|
*
|
596
|
* @see hook_file_insert()
|
597
|
* @see hook_file_update()
|
598
|
*/
|
599
|
function file_save(stdClass $file) {
|
600
|
$file->timestamp = REQUEST_TIME;
|
601
|
$file->filesize = filesize($file->uri);
|
602
|
|
603
|
// Load the stored entity, if any.
|
604
|
if (!empty($file->fid) && !isset($file->original)) {
|
605
|
$file->original = entity_load_unchanged('file', $file->fid);
|
606
|
}
|
607
|
|
608
|
module_invoke_all('file_presave', $file);
|
609
|
module_invoke_all('entity_presave', $file, 'file');
|
610
|
|
611
|
if (empty($file->fid)) {
|
612
|
drupal_write_record('file_managed', $file);
|
613
|
// Inform modules about the newly added file.
|
614
|
module_invoke_all('file_insert', $file);
|
615
|
module_invoke_all('entity_insert', $file, 'file');
|
616
|
}
|
617
|
else {
|
618
|
drupal_write_record('file_managed', $file, 'fid');
|
619
|
// Inform modules that the file has been updated.
|
620
|
module_invoke_all('file_update', $file);
|
621
|
module_invoke_all('entity_update', $file, 'file');
|
622
|
}
|
623
|
|
624
|
// Clear internal properties.
|
625
|
unset($file->original);
|
626
|
// Clear the static loading cache.
|
627
|
entity_get_controller('file')->resetCache(array($file->fid));
|
628
|
|
629
|
return $file;
|
630
|
}
|
631
|
|
632
|
/**
|
633
|
* Determines where a file is used.
|
634
|
*
|
635
|
* @param $file
|
636
|
* A file object.
|
637
|
*
|
638
|
* @return
|
639
|
* A nested array with usage data. The first level is keyed by module name,
|
640
|
* the second by object type and the third by the object id. The value
|
641
|
* of the third level contains the usage count.
|
642
|
*
|
643
|
* @see file_usage_add()
|
644
|
* @see file_usage_delete()
|
645
|
*/
|
646
|
function file_usage_list(stdClass $file) {
|
647
|
$result = db_select('file_usage', 'f')
|
648
|
->fields('f', array('module', 'type', 'id', 'count'))
|
649
|
->condition('fid', $file->fid)
|
650
|
->condition('count', 0, '>')
|
651
|
->execute();
|
652
|
$references = array();
|
653
|
foreach ($result as $usage) {
|
654
|
$references[$usage->module][$usage->type][$usage->id] = $usage->count;
|
655
|
}
|
656
|
return $references;
|
657
|
}
|
658
|
|
659
|
/**
|
660
|
* Records that a module is using a file.
|
661
|
*
|
662
|
* This usage information will be queried during file_delete() to ensure that
|
663
|
* a file is not in use before it is physically removed from disk.
|
664
|
*
|
665
|
* Examples:
|
666
|
* - A module that associates files with nodes, so $type would be
|
667
|
* 'node' and $id would be the node's nid. Files for all revisions are stored
|
668
|
* within a single nid.
|
669
|
* - The User module associates an image with a user, so $type would be 'user'
|
670
|
* and the $id would be the user's uid.
|
671
|
*
|
672
|
* @param $file
|
673
|
* A file object.
|
674
|
* @param $module
|
675
|
* The name of the module using the file.
|
676
|
* @param $type
|
677
|
* The type of the object that contains the referenced file.
|
678
|
* @param $id
|
679
|
* The unique, numeric ID of the object containing the referenced file.
|
680
|
* @param $count
|
681
|
* (optional) The number of references to add to the object. Defaults to 1.
|
682
|
*
|
683
|
* @see file_usage_list()
|
684
|
* @see file_usage_delete()
|
685
|
*/
|
686
|
function file_usage_add(stdClass $file, $module, $type, $id, $count = 1) {
|
687
|
db_merge('file_usage')
|
688
|
->key(array(
|
689
|
'fid' => $file->fid,
|
690
|
'module' => $module,
|
691
|
'type' => $type,
|
692
|
'id' => $id,
|
693
|
))
|
694
|
->fields(array('count' => $count))
|
695
|
->expression('count', 'count + :count', array(':count' => $count))
|
696
|
->execute();
|
697
|
}
|
698
|
|
699
|
/**
|
700
|
* Removes a record to indicate that a module is no longer using a file.
|
701
|
*
|
702
|
* The file_delete() function is typically called after removing a file usage
|
703
|
* to remove the record from the file_managed table and delete the file itself.
|
704
|
*
|
705
|
* @param $file
|
706
|
* A file object.
|
707
|
* @param $module
|
708
|
* The name of the module using the file.
|
709
|
* @param $type
|
710
|
* (optional) The type of the object that contains the referenced file. May
|
711
|
* be omitted if all module references to a file are being deleted.
|
712
|
* @param $id
|
713
|
* (optional) The unique, numeric ID of the object containing the referenced
|
714
|
* file. May be omitted if all module references to a file are being deleted.
|
715
|
* @param $count
|
716
|
* (optional) The number of references to delete from the object. Defaults to
|
717
|
* 1. 0 may be specified to delete all references to the file within a
|
718
|
* specific object.
|
719
|
*
|
720
|
* @see file_usage_add()
|
721
|
* @see file_usage_list()
|
722
|
* @see file_delete()
|
723
|
*/
|
724
|
function file_usage_delete(stdClass $file, $module, $type = NULL, $id = NULL, $count = 1) {
|
725
|
// Delete rows that have a exact or less value to prevent empty rows.
|
726
|
$query = db_delete('file_usage')
|
727
|
->condition('module', $module)
|
728
|
->condition('fid', $file->fid);
|
729
|
if ($type && $id) {
|
730
|
$query
|
731
|
->condition('type', $type)
|
732
|
->condition('id', $id);
|
733
|
}
|
734
|
if ($count) {
|
735
|
$query->condition('count', $count, '<=');
|
736
|
}
|
737
|
$result = $query->execute();
|
738
|
|
739
|
// If the row has more than the specified count decrement it by that number.
|
740
|
if (!$result && $count > 0) {
|
741
|
$query = db_update('file_usage')
|
742
|
->condition('module', $module)
|
743
|
->condition('fid', $file->fid);
|
744
|
if ($type && $id) {
|
745
|
$query
|
746
|
->condition('type', $type)
|
747
|
->condition('id', $id);
|
748
|
}
|
749
|
$query->expression('count', 'count - :count', array(':count' => $count));
|
750
|
$query->execute();
|
751
|
}
|
752
|
}
|
753
|
|
754
|
/**
|
755
|
* Copies a file to a new location and adds a file record to the database.
|
756
|
*
|
757
|
* This function should be used when manipulating files that have records
|
758
|
* stored in the database. This is a powerful function that in many ways
|
759
|
* performs like an advanced version of copy().
|
760
|
* - Checks if $source and $destination are valid and readable/writable.
|
761
|
* - If file already exists in $destination either the call will error out,
|
762
|
* replace the file or rename the file based on the $replace parameter.
|
763
|
* - If the $source and $destination are equal, the behavior depends on the
|
764
|
* $replace parameter. FILE_EXISTS_REPLACE will error out. FILE_EXISTS_RENAME
|
765
|
* will rename the file until the $destination is unique.
|
766
|
* - Adds the new file to the files database. If the source file is a
|
767
|
* temporary file, the resulting file will also be a temporary file. See
|
768
|
* file_save_upload() for details on temporary files.
|
769
|
*
|
770
|
* @param $source
|
771
|
* A file object.
|
772
|
* @param $destination
|
773
|
* A string containing the destination that $source should be copied to.
|
774
|
* This must be a stream wrapper URI.
|
775
|
* @param $replace
|
776
|
* Replace behavior when the destination file already exists:
|
777
|
* - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
|
778
|
* the destination name exists then its database entry will be updated. If
|
779
|
* no database entry is found then a new one will be created.
|
780
|
* - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
|
781
|
* unique.
|
782
|
* - FILE_EXISTS_ERROR - Do nothing and return FALSE.
|
783
|
*
|
784
|
* @return
|
785
|
* File object if the copy is successful, or FALSE in the event of an error.
|
786
|
*
|
787
|
* @see file_unmanaged_copy()
|
788
|
* @see hook_file_copy()
|
789
|
*/
|
790
|
function file_copy(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
|
791
|
if (!file_valid_uri($destination)) {
|
792
|
if (($realpath = drupal_realpath($source->uri)) !== FALSE) {
|
793
|
watchdog('file', 'File %file (%realpath) could not be copied, because the destination %destination is invalid. This is often caused by improper use of file_copy() or a missing stream wrapper.', array('%file' => $source->uri, '%realpath' => $realpath, '%destination' => $destination));
|
794
|
}
|
795
|
else {
|
796
|
watchdog('file', 'File %file could not be copied, because the destination %destination is invalid. This is often caused by improper use of file_copy() or a missing stream wrapper.', array('%file' => $source->uri, '%destination' => $destination));
|
797
|
}
|
798
|
drupal_set_message(t('The specified file %file could not be copied, because the destination is invalid. More information is available in the system log.', array('%file' => $source->uri)), 'error');
|
799
|
return FALSE;
|
800
|
}
|
801
|
|
802
|
if ($uri = file_unmanaged_copy($source->uri, $destination, $replace)) {
|
803
|
$file = clone $source;
|
804
|
$file->fid = NULL;
|
805
|
$file->uri = $uri;
|
806
|
$file->filename = drupal_basename($uri);
|
807
|
// If we are replacing an existing file re-use its database record.
|
808
|
if ($replace == FILE_EXISTS_REPLACE) {
|
809
|
$existing_files = file_load_multiple(array(), array('uri' => $uri));
|
810
|
if (count($existing_files)) {
|
811
|
$existing = reset($existing_files);
|
812
|
$file->fid = $existing->fid;
|
813
|
$file->filename = $existing->filename;
|
814
|
}
|
815
|
}
|
816
|
// If we are renaming around an existing file (rather than a directory),
|
817
|
// use its basename for the filename.
|
818
|
elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
|
819
|
$file->filename = drupal_basename($destination);
|
820
|
}
|
821
|
|
822
|
$file = file_save($file);
|
823
|
|
824
|
// Inform modules that the file has been copied.
|
825
|
module_invoke_all('file_copy', $file, $source);
|
826
|
|
827
|
return $file;
|
828
|
}
|
829
|
return FALSE;
|
830
|
}
|
831
|
|
832
|
/**
|
833
|
* Determines whether the URI has a valid scheme for file API operations.
|
834
|
*
|
835
|
* There must be a scheme and it must be a Drupal-provided scheme like
|
836
|
* 'public', 'private', 'temporary', or an extension provided with
|
837
|
* hook_stream_wrappers().
|
838
|
*
|
839
|
* @param $uri
|
840
|
* The URI to be tested.
|
841
|
*
|
842
|
* @return
|
843
|
* TRUE if the URI is allowed.
|
844
|
*/
|
845
|
function file_valid_uri($uri) {
|
846
|
// Assert that the URI has an allowed scheme. Barepaths are not allowed.
|
847
|
$uri_scheme = file_uri_scheme($uri);
|
848
|
if (empty($uri_scheme) || !file_stream_wrapper_valid_scheme($uri_scheme)) {
|
849
|
return FALSE;
|
850
|
}
|
851
|
return TRUE;
|
852
|
}
|
853
|
|
854
|
/**
|
855
|
* Copies a file to a new location without invoking the file API.
|
856
|
*
|
857
|
* This is a powerful function that in many ways performs like an advanced
|
858
|
* version of copy().
|
859
|
* - Checks if $source and $destination are valid and readable/writable.
|
860
|
* - If file already exists in $destination either the call will error out,
|
861
|
* replace the file or rename the file based on the $replace parameter.
|
862
|
* - If the $source and $destination are equal, the behavior depends on the
|
863
|
* $replace parameter. FILE_EXISTS_REPLACE will error out. FILE_EXISTS_RENAME
|
864
|
* will rename the file until the $destination is unique.
|
865
|
* - Provides a fallback using realpaths if the move fails using stream
|
866
|
* wrappers. This can occur because PHP's copy() function does not properly
|
867
|
* support streams if safe_mode or open_basedir are enabled. See
|
868
|
* https://bugs.php.net/bug.php?id=60456
|
869
|
*
|
870
|
* @param $source
|
871
|
* A string specifying the filepath or URI of the source file.
|
872
|
* @param $destination
|
873
|
* A URI containing the destination that $source should be copied to. The
|
874
|
* URI may be a bare filepath (without a scheme). If this value is omitted,
|
875
|
* Drupal's default files scheme will be used, usually "public://".
|
876
|
* @param $replace
|
877
|
* Replace behavior when the destination file already exists:
|
878
|
* - FILE_EXISTS_REPLACE - Replace the existing file.
|
879
|
* - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
|
880
|
* unique.
|
881
|
* - FILE_EXISTS_ERROR - Do nothing and return FALSE.
|
882
|
*
|
883
|
* @return
|
884
|
* The path to the new file, or FALSE in the event of an error.
|
885
|
*
|
886
|
* @see file_copy()
|
887
|
*/
|
888
|
function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
|
889
|
$original_source = $source;
|
890
|
$original_destination = $destination;
|
891
|
|
892
|
// Assert that the source file actually exists.
|
893
|
if (!file_exists($source)) {
|
894
|
// @todo Replace drupal_set_message() calls with exceptions instead.
|
895
|
drupal_set_message(t('The specified file %file could not be copied, because no file by that name exists. Please check that you supplied the correct filename.', array('%file' => $original_source)), 'error');
|
896
|
if (($realpath = drupal_realpath($original_source)) !== FALSE) {
|
897
|
watchdog('file', 'File %file (%realpath) could not be copied because it does not exist.', array('%file' => $original_source, '%realpath' => $realpath));
|
898
|
}
|
899
|
else {
|
900
|
watchdog('file', 'File %file could not be copied because it does not exist.', array('%file' => $original_source));
|
901
|
}
|
902
|
return FALSE;
|
903
|
}
|
904
|
|
905
|
// Build a destination URI if necessary.
|
906
|
if (!isset($destination)) {
|
907
|
$destination = file_build_uri(drupal_basename($source));
|
908
|
}
|
909
|
|
910
|
|
911
|
// Prepare the destination directory.
|
912
|
if (file_prepare_directory($destination)) {
|
913
|
// The destination is already a directory, so append the source basename.
|
914
|
$destination = file_stream_wrapper_uri_normalize($destination . '/' . drupal_basename($source));
|
915
|
}
|
916
|
else {
|
917
|
// Perhaps $destination is a dir/file?
|
918
|
$dirname = drupal_dirname($destination);
|
919
|
if (!file_prepare_directory($dirname)) {
|
920
|
// The destination is not valid.
|
921
|
watchdog('file', 'File %file could not be copied, because the destination directory %destination is not configured correctly.', array('%file' => $original_source, '%destination' => $dirname));
|
922
|
drupal_set_message(t('The specified file %file could not be copied, because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions. More information is available in the system log.', array('%file' => $original_source)), 'error');
|
923
|
return FALSE;
|
924
|
}
|
925
|
}
|
926
|
|
927
|
// Determine whether we can perform this operation based on overwrite rules.
|
928
|
$destination = file_destination($destination, $replace);
|
929
|
if ($destination === FALSE) {
|
930
|
drupal_set_message(t('The file %file could not be copied because a file by that name already exists in the destination directory.', array('%file' => $original_source)), 'error');
|
931
|
watchdog('file', 'File %file could not be copied because a file by that name already exists in the destination directory (%directory)', array('%file' => $original_source, '%directory' => $destination));
|
932
|
return FALSE;
|
933
|
}
|
934
|
|
935
|
// Assert that the source and destination filenames are not the same.
|
936
|
$real_source = drupal_realpath($source);
|
937
|
$real_destination = drupal_realpath($destination);
|
938
|
if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) {
|
939
|
drupal_set_message(t('The specified file %file was not copied because it would overwrite itself.', array('%file' => $source)), 'error');
|
940
|
watchdog('file', 'File %file could not be copied because it would overwrite itself.', array('%file' => $source));
|
941
|
return FALSE;
|
942
|
}
|
943
|
// Make sure the .htaccess files are present.
|
944
|
file_ensure_htaccess();
|
945
|
// Perform the copy operation.
|
946
|
if (!@copy($source, $destination)) {
|
947
|
// If the copy failed and realpaths exist, retry the operation using them
|
948
|
// instead.
|
949
|
if ($real_source === FALSE || $real_destination === FALSE || !@copy($real_source, $real_destination)) {
|
950
|
watchdog('file', 'The specified file %file could not be copied to %destination.', array('%file' => $source, '%destination' => $destination), WATCHDOG_ERROR);
|
951
|
return FALSE;
|
952
|
}
|
953
|
}
|
954
|
|
955
|
// Set the permissions on the new file.
|
956
|
drupal_chmod($destination);
|
957
|
|
958
|
return $destination;
|
959
|
}
|
960
|
|
961
|
/**
|
962
|
* Constructs a URI to Drupal's default files location given a relative path.
|
963
|
*/
|
964
|
function file_build_uri($path) {
|
965
|
$uri = file_default_scheme() . '://' . $path;
|
966
|
return file_stream_wrapper_uri_normalize($uri);
|
967
|
}
|
968
|
|
969
|
/**
|
970
|
* Determines the destination path for a file.
|
971
|
*
|
972
|
* @param $destination
|
973
|
* A string specifying the desired final URI or filepath.
|
974
|
* @param $replace
|
975
|
* Replace behavior when the destination file already exists.
|
976
|
* - FILE_EXISTS_REPLACE - Replace the existing file.
|
977
|
* - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
|
978
|
* unique.
|
979
|
* - FILE_EXISTS_ERROR - Do nothing and return FALSE.
|
980
|
*
|
981
|
* @return
|
982
|
* The destination filepath, or FALSE if the file already exists
|
983
|
* and FILE_EXISTS_ERROR is specified.
|
984
|
*/
|
985
|
function file_destination($destination, $replace) {
|
986
|
if (file_exists($destination)) {
|
987
|
switch ($replace) {
|
988
|
case FILE_EXISTS_REPLACE:
|
989
|
// Do nothing here, we want to overwrite the existing file.
|
990
|
break;
|
991
|
|
992
|
case FILE_EXISTS_RENAME:
|
993
|
$basename = drupal_basename($destination);
|
994
|
$directory = drupal_dirname($destination);
|
995
|
$destination = file_create_filename($basename, $directory);
|
996
|
break;
|
997
|
|
998
|
case FILE_EXISTS_ERROR:
|
999
|
// Error reporting handled by calling function.
|
1000
|
return FALSE;
|
1001
|
}
|
1002
|
}
|
1003
|
return $destination;
|
1004
|
}
|
1005
|
|
1006
|
/**
|
1007
|
* Moves a file to a new location and update the file's database entry.
|
1008
|
*
|
1009
|
* Moving a file is performed by copying the file to the new location and then
|
1010
|
* deleting the original.
|
1011
|
* - Checks if $source and $destination are valid and readable/writable.
|
1012
|
* - Performs a file move if $source is not equal to $destination.
|
1013
|
* - If file already exists in $destination either the call will error out,
|
1014
|
* replace the file or rename the file based on the $replace parameter.
|
1015
|
* - Adds the new file to the files database.
|
1016
|
*
|
1017
|
* @param $source
|
1018
|
* A file object.
|
1019
|
* @param $destination
|
1020
|
* A string containing the destination that $source should be moved to.
|
1021
|
* This must be a stream wrapper URI.
|
1022
|
* @param $replace
|
1023
|
* Replace behavior when the destination file already exists:
|
1024
|
* - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
|
1025
|
* the destination name exists then its database entry will be updated and
|
1026
|
* file_delete() called on the source file after hook_file_move is called.
|
1027
|
* If no database entry is found then the source files record will be
|
1028
|
* updated.
|
1029
|
* - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
|
1030
|
* unique.
|
1031
|
* - FILE_EXISTS_ERROR - Do nothing and return FALSE.
|
1032
|
*
|
1033
|
* @return
|
1034
|
* Resulting file object for success, or FALSE in the event of an error.
|
1035
|
*
|
1036
|
* @see file_unmanaged_move()
|
1037
|
* @see hook_file_move()
|
1038
|
*/
|
1039
|
function file_move(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
|
1040
|
if (!file_valid_uri($destination)) {
|
1041
|
if (($realpath = drupal_realpath($source->uri)) !== FALSE) {
|
1042
|
watchdog('file', 'File %file (%realpath) could not be moved, because the destination %destination is invalid. This may be caused by improper use of file_move() or a missing stream wrapper.', array('%file' => $source->uri, '%realpath' => $realpath, '%destination' => $destination));
|
1043
|
}
|
1044
|
else {
|
1045
|
watchdog('file', 'File %file could not be moved, because the destination %destination is invalid. This may be caused by improper use of file_move() or a missing stream wrapper.', array('%file' => $source->uri, '%destination' => $destination));
|
1046
|
}
|
1047
|
drupal_set_message(t('The specified file %file could not be moved, because the destination is invalid. More information is available in the system log.', array('%file' => $source->uri)), 'error');
|
1048
|
return FALSE;
|
1049
|
}
|
1050
|
|
1051
|
if ($uri = file_unmanaged_move($source->uri, $destination, $replace)) {
|
1052
|
$delete_source = FALSE;
|
1053
|
|
1054
|
$file = clone $source;
|
1055
|
$file->uri = $uri;
|
1056
|
// If we are replacing an existing file re-use its database record.
|
1057
|
if ($replace == FILE_EXISTS_REPLACE) {
|
1058
|
$existing_files = file_load_multiple(array(), array('uri' => $uri));
|
1059
|
if (count($existing_files)) {
|
1060
|
$existing = reset($existing_files);
|
1061
|
$delete_source = TRUE;
|
1062
|
$file->fid = $existing->fid;
|
1063
|
}
|
1064
|
}
|
1065
|
// If we are renaming around an existing file (rather than a directory),
|
1066
|
// use its basename for the filename.
|
1067
|
elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
|
1068
|
$file->filename = drupal_basename($destination);
|
1069
|
}
|
1070
|
|
1071
|
$file = file_save($file);
|
1072
|
|
1073
|
// Inform modules that the file has been moved.
|
1074
|
module_invoke_all('file_move', $file, $source);
|
1075
|
|
1076
|
if ($delete_source) {
|
1077
|
// Try a soft delete to remove original if it's not in use elsewhere.
|
1078
|
file_delete($source);
|
1079
|
}
|
1080
|
|
1081
|
return $file;
|
1082
|
}
|
1083
|
return FALSE;
|
1084
|
}
|
1085
|
|
1086
|
/**
|
1087
|
* Moves a file to a new location without database changes or hook invocation.
|
1088
|
*
|
1089
|
* @param $source
|
1090
|
* A string specifying the filepath or URI of the original file.
|
1091
|
* @param $destination
|
1092
|
* A string containing the destination that $source should be moved to.
|
1093
|
* This must be a stream wrapper URI. If this value is omitted, Drupal's
|
1094
|
* default files scheme will be used, usually "public://".
|
1095
|
* @param $replace
|
1096
|
* Replace behavior when the destination file already exists:
|
1097
|
* - FILE_EXISTS_REPLACE - Replace the existing file.
|
1098
|
* - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
|
1099
|
* unique.
|
1100
|
* - FILE_EXISTS_ERROR - Do nothing and return FALSE.
|
1101
|
*
|
1102
|
* @return
|
1103
|
* The URI of the moved file, or FALSE in the event of an error.
|
1104
|
*
|
1105
|
* @see file_move()
|
1106
|
*/
|
1107
|
function file_unmanaged_move($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
|
1108
|
$filepath = file_unmanaged_copy($source, $destination, $replace);
|
1109
|
if ($filepath == FALSE || file_unmanaged_delete($source) == FALSE) {
|
1110
|
return FALSE;
|
1111
|
}
|
1112
|
return $filepath;
|
1113
|
}
|
1114
|
|
1115
|
/**
|
1116
|
* Modifies a filename as needed for security purposes.
|
1117
|
*
|
1118
|
* Munging a file name prevents unknown file extensions from masking exploit
|
1119
|
* files. When web servers such as Apache decide how to process a URL request,
|
1120
|
* they use the file extension. If the extension is not recognized, Apache
|
1121
|
* skips that extension and uses the previous file extension. For example, if
|
1122
|
* the file being requested is exploit.php.pps, and Apache does not recognize
|
1123
|
* the '.pps' extension, it treats the file as PHP and executes it. To make
|
1124
|
* this file name safe for Apache and prevent it from executing as PHP, the
|
1125
|
* .php extension is "munged" into .php_, making the safe file name
|
1126
|
* exploit.php_.pps.
|
1127
|
*
|
1128
|
* Specifically, this function adds an underscore to all extensions that are
|
1129
|
* between 2 and 5 characters in length, internal to the file name, and not
|
1130
|
* included in $extensions.
|
1131
|
*
|
1132
|
* Function behavior is also controlled by the Drupal variable
|
1133
|
* 'allow_insecure_uploads'. If 'allow_insecure_uploads' evaluates to TRUE, no
|
1134
|
* alterations will be made, if it evaluates to FALSE, the filename is 'munged'.
|
1135
|
*
|
1136
|
* @param $filename
|
1137
|
* File name to modify.
|
1138
|
* @param $extensions
|
1139
|
* A space-separated list of extensions that should not be altered.
|
1140
|
* @param $alerts
|
1141
|
* If TRUE, drupal_set_message() will be called to display a message if the
|
1142
|
* file name was changed.
|
1143
|
*
|
1144
|
* @return
|
1145
|
* The potentially modified $filename.
|
1146
|
*/
|
1147
|
function file_munge_filename($filename, $extensions, $alerts = TRUE) {
|
1148
|
$original = $filename;
|
1149
|
|
1150
|
// Allow potentially insecure uploads for very savvy users and admin
|
1151
|
if (!variable_get('allow_insecure_uploads', 0)) {
|
1152
|
// Remove any null bytes. See http://php.net/manual/security.filesystem.nullbytes.php
|
1153
|
$filename = str_replace(chr(0), '', $filename);
|
1154
|
|
1155
|
$whitelist = array_unique(explode(' ', strtolower(trim($extensions))));
|
1156
|
|
1157
|
// Split the filename up by periods. The first part becomes the basename
|
1158
|
// the last part the final extension.
|
1159
|
$filename_parts = explode('.', $filename);
|
1160
|
$new_filename = array_shift($filename_parts); // Remove file basename.
|
1161
|
$final_extension = array_pop($filename_parts); // Remove final extension.
|
1162
|
|
1163
|
// Loop through the middle parts of the name and add an underscore to the
|
1164
|
// end of each section that could be a file extension but isn't in the list
|
1165
|
// of allowed extensions.
|
1166
|
foreach ($filename_parts as $filename_part) {
|
1167
|
$new_filename .= '.' . $filename_part;
|
1168
|
if (!in_array(strtolower($filename_part), $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
|
1169
|
$new_filename .= '_';
|
1170
|
}
|
1171
|
}
|
1172
|
$filename = $new_filename . '.' . $final_extension;
|
1173
|
|
1174
|
if ($alerts && $original != $filename) {
|
1175
|
drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $filename)));
|
1176
|
}
|
1177
|
}
|
1178
|
|
1179
|
return $filename;
|
1180
|
}
|
1181
|
|
1182
|
/**
|
1183
|
* Undoes the effect of file_munge_filename().
|
1184
|
*
|
1185
|
* @param $filename
|
1186
|
* String with the filename to be unmunged.
|
1187
|
*
|
1188
|
* @return
|
1189
|
* An unmunged filename string.
|
1190
|
*/
|
1191
|
function file_unmunge_filename($filename) {
|
1192
|
return str_replace('_.', '.', $filename);
|
1193
|
}
|
1194
|
|
1195
|
/**
|
1196
|
* Creates a full file path from a directory and filename.
|
1197
|
*
|
1198
|
* If a file with the specified name already exists, an alternative will be
|
1199
|
* used.
|
1200
|
*
|
1201
|
* @param $basename
|
1202
|
* String filename
|
1203
|
* @param $directory
|
1204
|
* String containing the directory or parent URI.
|
1205
|
*
|
1206
|
* @return
|
1207
|
* File path consisting of $directory and a unique filename based off
|
1208
|
* of $basename.
|
1209
|
*/
|
1210
|
function file_create_filename($basename, $directory) {
|
1211
|
// Strip control characters (ASCII value < 32). Though these are allowed in
|
1212
|
// some filesystems, not many applications handle them well.
|
1213
|
$basename = preg_replace('/[\x00-\x1F]/u', '_', $basename);
|
1214
|
if (substr(PHP_OS, 0, 3) == 'WIN') {
|
1215
|
// These characters are not allowed in Windows filenames
|
1216
|
$basename = str_replace(array(':', '*', '?', '"', '<', '>', '|'), '_', $basename);
|
1217
|
}
|
1218
|
|
1219
|
// A URI or path may already have a trailing slash or look like "public://".
|
1220
|
if (substr($directory, -1) == '/') {
|
1221
|
$separator = '';
|
1222
|
}
|
1223
|
else {
|
1224
|
$separator = '/';
|
1225
|
}
|
1226
|
|
1227
|
$destination = $directory . $separator . $basename;
|
1228
|
|
1229
|
if (file_exists($destination)) {
|
1230
|
// Destination file already exists, generate an alternative.
|
1231
|
$pos = strrpos($basename, '.');
|
1232
|
if ($pos !== FALSE) {
|
1233
|
$name = substr($basename, 0, $pos);
|
1234
|
$ext = substr($basename, $pos);
|
1235
|
}
|
1236
|
else {
|
1237
|
$name = $basename;
|
1238
|
$ext = '';
|
1239
|
}
|
1240
|
|
1241
|
$counter = 0;
|
1242
|
do {
|
1243
|
$destination = $directory . $separator . $name . '_' . $counter++ . $ext;
|
1244
|
} while (file_exists($destination));
|
1245
|
}
|
1246
|
|
1247
|
return $destination;
|
1248
|
}
|
1249
|
|
1250
|
/**
|
1251
|
* Deletes a file and its database record.
|
1252
|
*
|
1253
|
* If the $force parameter is not TRUE, file_usage_list() will be called to
|
1254
|
* determine if the file is being used by any modules. If the file is being
|
1255
|
* used the delete will be canceled.
|
1256
|
*
|
1257
|
* @param $file
|
1258
|
* A file object.
|
1259
|
* @param $force
|
1260
|
* Boolean indicating that the file should be deleted even if the file is
|
1261
|
* reported as in use by the file_usage table.
|
1262
|
*
|
1263
|
* @return mixed
|
1264
|
* TRUE for success, FALSE in the event of an error, or an array if the file
|
1265
|
* is being used by any modules.
|
1266
|
*
|
1267
|
* @see file_unmanaged_delete()
|
1268
|
* @see file_usage_list()
|
1269
|
* @see file_usage_delete()
|
1270
|
* @see hook_file_delete()
|
1271
|
*/
|
1272
|
function file_delete(stdClass $file, $force = FALSE) {
|
1273
|
if (!file_valid_uri($file->uri)) {
|
1274
|
if (($realpath = drupal_realpath($file->uri)) !== FALSE) {
|
1275
|
watchdog('file', 'File %file (%realpath) could not be deleted because it is not a valid URI. This may be caused by improper use of file_delete() or a missing stream wrapper.', array('%file' => $file->uri, '%realpath' => $realpath));
|
1276
|
}
|
1277
|
else {
|
1278
|
watchdog('file', 'File %file could not be deleted because it is not a valid URI. This may be caused by improper use of file_delete() or a missing stream wrapper.', array('%file' => $file->uri));
|
1279
|
}
|
1280
|
drupal_set_message(t('The specified file %file could not be deleted, because it is not a valid URI. More information is available in the system log.', array('%file' => $file->uri)), 'error');
|
1281
|
return FALSE;
|
1282
|
}
|
1283
|
|
1284
|
// If any module still has a usage entry in the file_usage table, the file
|
1285
|
// will not be deleted, but file_delete() will return a populated array
|
1286
|
// that tests as TRUE.
|
1287
|
if (!$force && ($references = file_usage_list($file))) {
|
1288
|
return $references;
|
1289
|
}
|
1290
|
|
1291
|
// Let other modules clean up any references to the deleted file.
|
1292
|
module_invoke_all('file_delete', $file);
|
1293
|
module_invoke_all('entity_delete', $file, 'file');
|
1294
|
|
1295
|
// Make sure the file is deleted before removing its row from the
|
1296
|
// database, so UIs can still find the file in the database.
|
1297
|
if (file_unmanaged_delete($file->uri)) {
|
1298
|
db_delete('file_managed')->condition('fid', $file->fid)->execute();
|
1299
|
db_delete('file_usage')->condition('fid', $file->fid)->execute();
|
1300
|
entity_get_controller('file')->resetCache();
|
1301
|
return TRUE;
|
1302
|
}
|
1303
|
return FALSE;
|
1304
|
}
|
1305
|
|
1306
|
/**
|
1307
|
* Deletes a file without database changes or hook invocations.
|
1308
|
*
|
1309
|
* This function should be used when the file to be deleted does not have an
|
1310
|
* entry recorded in the files table.
|
1311
|
*
|
1312
|
* @param $path
|
1313
|
* A string containing a file path or (streamwrapper) URI.
|
1314
|
*
|
1315
|
* @return
|
1316
|
* TRUE for success or path does not exist, or FALSE in the event of an
|
1317
|
* error.
|
1318
|
*
|
1319
|
* @see file_delete()
|
1320
|
* @see file_unmanaged_delete_recursive()
|
1321
|
*/
|
1322
|
function file_unmanaged_delete($path) {
|
1323
|
if (is_dir($path)) {
|
1324
|
watchdog('file', '%path is a directory and cannot be removed using file_unmanaged_delete().', array('%path' => $path), WATCHDOG_ERROR);
|
1325
|
return FALSE;
|
1326
|
}
|
1327
|
if (is_file($path)) {
|
1328
|
return drupal_unlink($path);
|
1329
|
}
|
1330
|
// Return TRUE for non-existent file, but log that nothing was actually
|
1331
|
// deleted, as the current state is the intended result.
|
1332
|
if (!file_exists($path)) {
|
1333
|
watchdog('file', 'The file %path was not deleted, because it does not exist.', array('%path' => $path), WATCHDOG_NOTICE);
|
1334
|
return TRUE;
|
1335
|
}
|
1336
|
// We cannot handle anything other than files and directories. Log an error
|
1337
|
// for everything else (sockets, symbolic links, etc).
|
1338
|
watchdog('file', 'The file %path is not of a recognized type so it was not deleted.', array('%path' => $path), WATCHDOG_ERROR);
|
1339
|
return FALSE;
|
1340
|
}
|
1341
|
|
1342
|
/**
|
1343
|
* Deletes all files and directories in the specified filepath recursively.
|
1344
|
*
|
1345
|
* If the specified path is a directory then the function will call itself
|
1346
|
* recursively to process the contents. Once the contents have been removed the
|
1347
|
* directory will also be removed.
|
1348
|
*
|
1349
|
* If the specified path is a file then it will be passed to
|
1350
|
* file_unmanaged_delete().
|
1351
|
*
|
1352
|
* Note that this only deletes visible files with write permission.
|
1353
|
*
|
1354
|
* @param $path
|
1355
|
* A string containing either an URI or a file or directory path.
|
1356
|
*
|
1357
|
* @return
|
1358
|
* TRUE for success or if path does not exist, FALSE in the event of an
|
1359
|
* error.
|
1360
|
*
|
1361
|
* @see file_unmanaged_delete()
|
1362
|
*/
|
1363
|
function file_unmanaged_delete_recursive($path) {
|
1364
|
if (is_dir($path)) {
|
1365
|
$dir = dir($path);
|
1366
|
while (($entry = $dir->read()) !== FALSE) {
|
1367
|
if ($entry == '.' || $entry == '..') {
|
1368
|
continue;
|
1369
|
}
|
1370
|
$entry_path = $path . '/' . $entry;
|
1371
|
file_unmanaged_delete_recursive($entry_path);
|
1372
|
}
|
1373
|
$dir->close();
|
1374
|
|
1375
|
return drupal_rmdir($path);
|
1376
|
}
|
1377
|
return file_unmanaged_delete($path);
|
1378
|
}
|
1379
|
|
1380
|
/**
|
1381
|
* Determines total disk space used by a single user or the whole filesystem.
|
1382
|
*
|
1383
|
* @param $uid
|
1384
|
* Optional. A user id, specifying NULL returns the total space used by all
|
1385
|
* non-temporary files.
|
1386
|
* @param $status
|
1387
|
* Optional. The file status to consider. The default is to only
|
1388
|
* consider files in status FILE_STATUS_PERMANENT.
|
1389
|
*
|
1390
|
* @return
|
1391
|
* An integer containing the number of bytes used.
|
1392
|
*/
|
1393
|
function file_space_used($uid = NULL, $status = FILE_STATUS_PERMANENT) {
|
1394
|
$query = db_select('file_managed', 'f');
|
1395
|
$query->condition('f.status', $status);
|
1396
|
$query->addExpression('SUM(f.filesize)', 'filesize');
|
1397
|
if (isset($uid)) {
|
1398
|
$query->condition('f.uid', $uid);
|
1399
|
}
|
1400
|
return $query->execute()->fetchField();
|
1401
|
}
|
1402
|
|
1403
|
/**
|
1404
|
* Saves a file upload to a new location.
|
1405
|
*
|
1406
|
* The file will be added to the {file_managed} table as a temporary file.
|
1407
|
* Temporary files are periodically cleaned. To make the file a permanent file,
|
1408
|
* assign the status and use file_save() to save the changes.
|
1409
|
*
|
1410
|
* @param $form_field_name
|
1411
|
* A string that is the associative array key of the upload form element in
|
1412
|
* the form array.
|
1413
|
* @param $validators
|
1414
|
* An optional, associative array of callback functions used to validate the
|
1415
|
* file. See file_validate() for a full discussion of the array format.
|
1416
|
* If no extension validator is provided it will default to a limited safe
|
1417
|
* list of extensions which is as follows: "jpg jpeg gif png txt
|
1418
|
* doc xls pdf ppt pps odt ods odp". To allow all extensions you must
|
1419
|
* explicitly set the 'file_validate_extensions' validator to an empty array
|
1420
|
* (Beware: this is not safe and should only be allowed for trusted users, if
|
1421
|
* at all).
|
1422
|
* @param $destination
|
1423
|
* A string containing the URI that the file should be copied to. This must
|
1424
|
* be a stream wrapper URI. If this value is omitted, Drupal's temporary
|
1425
|
* files scheme will be used ("temporary://").
|
1426
|
* @param $replace
|
1427
|
* Replace behavior when the destination file already exists:
|
1428
|
* - FILE_EXISTS_REPLACE: Replace the existing file.
|
1429
|
* - FILE_EXISTS_RENAME: Append _{incrementing number} until the filename is
|
1430
|
* unique.
|
1431
|
* - FILE_EXISTS_ERROR: Do nothing and return FALSE.
|
1432
|
*
|
1433
|
* @return
|
1434
|
* An object containing the file information if the upload succeeded, FALSE
|
1435
|
* in the event of an error, or NULL if no file was uploaded. The
|
1436
|
* documentation for the "File interface" group, which you can find under
|
1437
|
* Related topics, or the header at the top of this file, documents the
|
1438
|
* components of a file object. In addition to the standard components,
|
1439
|
* this function adds:
|
1440
|
* - source: Path to the file before it is moved.
|
1441
|
* - destination: Path to the file after it is moved (same as 'uri').
|
1442
|
*/
|
1443
|
function file_save_upload($form_field_name, $validators = array(), $destination = FALSE, $replace = FILE_EXISTS_RENAME) {
|
1444
|
global $user;
|
1445
|
static $upload_cache;
|
1446
|
|
1447
|
// Return cached objects without processing since the file will have
|
1448
|
// already been processed and the paths in _FILES will be invalid.
|
1449
|
if (isset($upload_cache[$form_field_name])) {
|
1450
|
return $upload_cache[$form_field_name];
|
1451
|
}
|
1452
|
|
1453
|
// Make sure there's an upload to process.
|
1454
|
if (empty($_FILES['files']['name'][$form_field_name])) {
|
1455
|
return NULL;
|
1456
|
}
|
1457
|
|
1458
|
// Check for file upload errors and return FALSE if a lower level system
|
1459
|
// error occurred. For a complete list of errors:
|
1460
|
// See http://php.net/manual/features.file-upload.errors.php.
|
1461
|
switch ($_FILES['files']['error'][$form_field_name]) {
|
1462
|
case UPLOAD_ERR_INI_SIZE:
|
1463
|
case UPLOAD_ERR_FORM_SIZE:
|
1464
|
drupal_set_message(t('The file %file could not be saved, because it exceeds %maxsize, the maximum allowed size for uploads.', array('%file' => $_FILES['files']['name'][$form_field_name], '%maxsize' => format_size(file_upload_max_size()))), 'error');
|
1465
|
return FALSE;
|
1466
|
|
1467
|
case UPLOAD_ERR_PARTIAL:
|
1468
|
case UPLOAD_ERR_NO_FILE:
|
1469
|
drupal_set_message(t('The file %file could not be saved, because the upload did not complete.', array('%file' => $_FILES['files']['name'][$form_field_name])), 'error');
|
1470
|
return FALSE;
|
1471
|
|
1472
|
case UPLOAD_ERR_OK:
|
1473
|
// Final check that this is a valid upload, if it isn't, use the
|
1474
|
// default error handler.
|
1475
|
if (is_uploaded_file($_FILES['files']['tmp_name'][$form_field_name])) {
|
1476
|
break;
|
1477
|
}
|
1478
|
|
1479
|
// Unknown error
|
1480
|
default:
|
1481
|
drupal_set_message(t('The file %file could not be saved. An unknown error has occurred.', array('%file' => $_FILES['files']['name'][$form_field_name])), 'error');
|
1482
|
return FALSE;
|
1483
|
}
|
1484
|
|
1485
|
// Begin building file object.
|
1486
|
$file = new stdClass();
|
1487
|
$file->uid = $user->uid;
|
1488
|
$file->status = 0;
|
1489
|
$file->filename = trim(drupal_basename($_FILES['files']['name'][$form_field_name]), '.');
|
1490
|
$file->uri = $_FILES['files']['tmp_name'][$form_field_name];
|
1491
|
$file->filemime = file_get_mimetype($file->filename);
|
1492
|
$file->filesize = $_FILES['files']['size'][$form_field_name];
|
1493
|
|
1494
|
$extensions = '';
|
1495
|
if (isset($validators['file_validate_extensions'])) {
|
1496
|
if (isset($validators['file_validate_extensions'][0])) {
|
1497
|
// Build the list of non-munged extensions if the caller provided them.
|
1498
|
$extensions = $validators['file_validate_extensions'][0];
|
1499
|
}
|
1500
|
else {
|
1501
|
// If 'file_validate_extensions' is set and the list is empty then the
|
1502
|
// caller wants to allow any extension. In this case we have to remove the
|
1503
|
// validator or else it will reject all extensions.
|
1504
|
unset($validators['file_validate_extensions']);
|
1505
|
}
|
1506
|
}
|
1507
|
else {
|
1508
|
// No validator was provided, so add one using the default list.
|
1509
|
// Build a default non-munged safe list for file_munge_filename().
|
1510
|
$extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
|
1511
|
$validators['file_validate_extensions'] = array();
|
1512
|
$validators['file_validate_extensions'][0] = $extensions;
|
1513
|
}
|
1514
|
|
1515
|
if (!empty($extensions)) {
|
1516
|
// Munge the filename to protect against possible malicious extension hiding
|
1517
|
// within an unknown file type (ie: filename.html.foo).
|
1518
|
$file->filename = file_munge_filename($file->filename, $extensions);
|
1519
|
}
|
1520
|
|
1521
|
// Rename potentially executable files, to help prevent exploits (i.e. will
|
1522
|
// rename filename.php.foo and filename.php to filename.php.foo.txt and
|
1523
|
// filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
|
1524
|
// evaluates to TRUE.
|
1525
|
if (!variable_get('allow_insecure_uploads', 0) && preg_match('/\.(php|pl|py|cgi|asp|js)(\.|$)/i', $file->filename) && (substr($file->filename, -4) != '.txt')) {
|
1526
|
$file->filemime = 'text/plain';
|
1527
|
$file->uri .= '.txt';
|
1528
|
$file->filename .= '.txt';
|
1529
|
// The .txt extension may not be in the allowed list of extensions. We have
|
1530
|
// to add it here or else the file upload will fail.
|
1531
|
if (!empty($extensions)) {
|
1532
|
$validators['file_validate_extensions'][0] .= ' txt';
|
1533
|
drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $file->filename)));
|
1534
|
}
|
1535
|
}
|
1536
|
|
1537
|
// If the destination is not provided, use the temporary directory.
|
1538
|
if (empty($destination)) {
|
1539
|
$destination = 'temporary://';
|
1540
|
}
|
1541
|
|
1542
|
// Assert that the destination contains a valid stream.
|
1543
|
$destination_scheme = file_uri_scheme($destination);
|
1544
|
if (!$destination_scheme || !file_stream_wrapper_valid_scheme($destination_scheme)) {
|
1545
|
drupal_set_message(t('The file could not be uploaded, because the destination %destination is invalid.', array('%destination' => $destination)), 'error');
|
1546
|
return FALSE;
|
1547
|
}
|
1548
|
|
1549
|
$file->source = $form_field_name;
|
1550
|
// A URI may already have a trailing slash or look like "public://".
|
1551
|
if (substr($destination, -1) != '/') {
|
1552
|
$destination .= '/';
|
1553
|
}
|
1554
|
$file->destination = file_destination($destination . $file->filename, $replace);
|
1555
|
// If file_destination() returns FALSE then $replace == FILE_EXISTS_ERROR and
|
1556
|
// there's an existing file so we need to bail.
|
1557
|
if ($file->destination === FALSE) {
|
1558
|
drupal_set_message(t('The file %source could not be uploaded because a file by that name already exists in the destination %directory.', array('%source' => $form_field_name, '%directory' => $destination)), 'error');
|
1559
|
return FALSE;
|
1560
|
}
|
1561
|
|
1562
|
// Add in our check of the the file name length.
|
1563
|
$validators['file_validate_name_length'] = array();
|
1564
|
|
1565
|
// Call the validation functions specified by this function's caller.
|
1566
|
$errors = file_validate($file, $validators);
|
1567
|
|
1568
|
// Check for errors.
|
1569
|
if (!empty($errors)) {
|
1570
|
$message = t('The specified file %name could not be uploaded.', array('%name' => $file->filename));
|
1571
|
if (count($errors) > 1) {
|
1572
|
$message .= theme('item_list', array('items' => $errors));
|
1573
|
}
|
1574
|
else {
|
1575
|
$message .= ' ' . array_pop($errors);
|
1576
|
}
|
1577
|
form_set_error($form_field_name, $message);
|
1578
|
return FALSE;
|
1579
|
}
|
1580
|
|
1581
|
// Move uploaded files from PHP's upload_tmp_dir to Drupal's temporary
|
1582
|
// directory. This overcomes open_basedir restrictions for future file
|
1583
|
// operations.
|
1584
|
$file->uri = $file->destination;
|
1585
|
if (!drupal_move_uploaded_file($_FILES['files']['tmp_name'][$form_field_name], $file->uri)) {
|
1586
|
form_set_error($form_field_name, t('File upload error. Could not move uploaded file.'));
|
1587
|
watchdog('file', 'Upload error. Could not move uploaded file %file to destination %destination.', array('%file' => $file->filename, '%destination' => $file->uri));
|
1588
|
return FALSE;
|
1589
|
}
|
1590
|
|
1591
|
// Set the permissions on the new file.
|
1592
|
drupal_chmod($file->uri);
|
1593
|
|
1594
|
// If we are replacing an existing file re-use its database record.
|
1595
|
if ($replace == FILE_EXISTS_REPLACE) {
|
1596
|
$existing_files = file_load_multiple(array(), array('uri' => $file->uri));
|
1597
|
if (count($existing_files)) {
|
1598
|
$existing = reset($existing_files);
|
1599
|
$file->fid = $existing->fid;
|
1600
|
}
|
1601
|
}
|
1602
|
|
1603
|
// If we made it this far it's safe to record this file in the database.
|
1604
|
if ($file = file_save($file)) {
|
1605
|
// Add file to the cache.
|
1606
|
$upload_cache[$form_field_name] = $file;
|
1607
|
return $file;
|
1608
|
}
|
1609
|
return FALSE;
|
1610
|
}
|
1611
|
|
1612
|
/**
|
1613
|
* Moves an uploaded file to a new location.
|
1614
|
*
|
1615
|
* PHP's move_uploaded_file() does not properly support streams if safe_mode
|
1616
|
* or open_basedir are enabled, so this function fills that gap.
|
1617
|
*
|
1618
|
* Compatibility: normal paths and stream wrappers.
|
1619
|
*
|
1620
|
* @param $filename
|
1621
|
* The filename of the uploaded file.
|
1622
|
* @param $uri
|
1623
|
* A string containing the destination URI of the file.
|
1624
|
*
|
1625
|
* @return
|
1626
|
* TRUE on success, or FALSE on failure.
|
1627
|
*
|
1628
|
* @see move_uploaded_file()
|
1629
|
* @see http://drupal.org/node/515192
|
1630
|
* @ingroup php_wrappers
|
1631
|
*/
|
1632
|
function drupal_move_uploaded_file($filename, $uri) {
|
1633
|
$result = @move_uploaded_file($filename, $uri);
|
1634
|
// PHP's move_uploaded_file() does not properly support streams if safe_mode
|
1635
|
// or open_basedir are enabled so if the move failed, try finding a real path
|
1636
|
// and retry the move operation.
|
1637
|
if (!$result) {
|
1638
|
if ($realpath = drupal_realpath($uri)) {
|
1639
|
$result = move_uploaded_file($filename, $realpath);
|
1640
|
}
|
1641
|
else {
|
1642
|
$result = move_uploaded_file($filename, $uri);
|
1643
|
}
|
1644
|
}
|
1645
|
|
1646
|
return $result;
|
1647
|
}
|
1648
|
|
1649
|
/**
|
1650
|
* Checks that a file meets the criteria specified by the validators.
|
1651
|
*
|
1652
|
* After executing the validator callbacks specified hook_file_validate() will
|
1653
|
* also be called to allow other modules to report errors about the file.
|
1654
|
*
|
1655
|
* @param $file
|
1656
|
* A Drupal file object.
|
1657
|
* @param $validators
|
1658
|
* An optional, associative array of callback functions used to validate the
|
1659
|
* file. The keys are function names and the values arrays of callback
|
1660
|
* parameters which will be passed in after the file object. The
|
1661
|
* functions should return an array of error messages; an empty array
|
1662
|
* indicates that the file passed validation. The functions will be called in
|
1663
|
* the order specified.
|
1664
|
*
|
1665
|
* @return
|
1666
|
* An array containing validation error messages.
|
1667
|
*
|
1668
|
* @see hook_file_validate()
|
1669
|
*/
|
1670
|
function file_validate(stdClass &$file, $validators = array()) {
|
1671
|
// Call the validation functions specified by this function's caller.
|
1672
|
$errors = array();
|
1673
|
foreach ($validators as $function => $args) {
|
1674
|
if (function_exists($function)) {
|
1675
|
array_unshift($args, $file);
|
1676
|
$errors = array_merge($errors, call_user_func_array($function, $args));
|
1677
|
}
|
1678
|
}
|
1679
|
|
1680
|
// Let other modules perform validation on the new file.
|
1681
|
return array_merge($errors, module_invoke_all('file_validate', $file));
|
1682
|
}
|
1683
|
|
1684
|
/**
|
1685
|
* Checks for files with names longer than we can store in the database.
|
1686
|
*
|
1687
|
* @param $file
|
1688
|
* A Drupal file object.
|
1689
|
*
|
1690
|
* @return
|
1691
|
* An array. If the file name is too long, it will contain an error message.
|
1692
|
*/
|
1693
|
function file_validate_name_length(stdClass $file) {
|
1694
|
$errors = array();
|
1695
|
|
1696
|
if (empty($file->filename)) {
|
1697
|
$errors[] = t("The file's name is empty. Please give a name to the file.");
|
1698
|
}
|
1699
|
if (strlen($file->filename) > 240) {
|
1700
|
$errors[] = t("The file's name exceeds the 240 characters limit. Please rename the file and try again.");
|
1701
|
}
|
1702
|
return $errors;
|
1703
|
}
|
1704
|
|
1705
|
/**
|
1706
|
* Checks that the filename ends with an allowed extension.
|
1707
|
*
|
1708
|
* @param $file
|
1709
|
* A Drupal file object.
|
1710
|
* @param $extensions
|
1711
|
* A string with a space separated list of allowed extensions.
|
1712
|
*
|
1713
|
* @return
|
1714
|
* An array. If the file extension is not allowed, it will contain an error
|
1715
|
* message.
|
1716
|
*
|
1717
|
* @see hook_file_validate()
|
1718
|
*/
|
1719
|
function file_validate_extensions(stdClass $file, $extensions) {
|
1720
|
$errors = array();
|
1721
|
|
1722
|
$regex = '/\.(' . preg_replace('/ +/', '|', preg_quote($extensions)) . ')$/i';
|
1723
|
if (!preg_match($regex, $file->filename)) {
|
1724
|
$errors[] = t('Only files with the following extensions are allowed: %files-allowed.', array('%files-allowed' => $extensions));
|
1725
|
}
|
1726
|
return $errors;
|
1727
|
}
|
1728
|
|
1729
|
/**
|
1730
|
* Checks that the file's size is below certain limits.
|
1731
|
*
|
1732
|
* @param $file
|
1733
|
* A Drupal file object.
|
1734
|
* @param $file_limit
|
1735
|
* An integer specifying the maximum file size in bytes. Zero indicates that
|
1736
|
* no limit should be enforced.
|
1737
|
* @param $user_limit
|
1738
|
* An integer specifying the maximum number of bytes the user is allowed.
|
1739
|
* Zero indicates that no limit should be enforced.
|
1740
|
*
|
1741
|
* @return
|
1742
|
* An array. If the file size exceeds limits, it will contain an error
|
1743
|
* message.
|
1744
|
*
|
1745
|
* @see hook_file_validate()
|
1746
|
*/
|
1747
|
function file_validate_size(stdClass $file, $file_limit = 0, $user_limit = 0) {
|
1748
|
global $user;
|
1749
|
$errors = array();
|
1750
|
|
1751
|
if ($file_limit && $file->filesize > $file_limit) {
|
1752
|
$errors[] = t('The file is %filesize exceeding the maximum file size of %maxsize.', array('%filesize' => format_size($file->filesize), '%maxsize' => format_size($file_limit)));
|
1753
|
}
|
1754
|
|
1755
|
// Save a query by only calling file_space_used() when a limit is provided.
|
1756
|
if ($user_limit && (file_space_used($user->uid) + $file->filesize) > $user_limit) {
|
1757
|
$errors[] = t('The file is %filesize which would exceed your disk quota of %quota.', array('%filesize' => format_size($file->filesize), '%quota' => format_size($user_limit)));
|
1758
|
}
|
1759
|
|
1760
|
return $errors;
|
1761
|
}
|
1762
|
|
1763
|
/**
|
1764
|
* Checks that the file is recognized by image_get_info() as an image.
|
1765
|
*
|
1766
|
* @param $file
|
1767
|
* A Drupal file object.
|
1768
|
*
|
1769
|
* @return
|
1770
|
* An array. If the file is not an image, it will contain an error message.
|
1771
|
*
|
1772
|
* @see hook_file_validate()
|
1773
|
*/
|
1774
|
function file_validate_is_image(stdClass $file) {
|
1775
|
$errors = array();
|
1776
|
|
1777
|
$info = image_get_info($file->uri);
|
1778
|
if (!$info || empty($info['extension'])) {
|
1779
|
$errors[] = t('Only JPEG, PNG and GIF images are allowed.');
|
1780
|
}
|
1781
|
|
1782
|
return $errors;
|
1783
|
}
|
1784
|
|
1785
|
/**
|
1786
|
* Verifies that image dimensions are within the specified maximum and minimum.
|
1787
|
*
|
1788
|
* Non-image files will be ignored. If a image toolkit is available the image
|
1789
|
* will be scaled to fit within the desired maximum dimensions.
|
1790
|
*
|
1791
|
* @param $file
|
1792
|
* A Drupal file object. This function may resize the file affecting its
|
1793
|
* size.
|
1794
|
* @param $maximum_dimensions
|
1795
|
* An optional string in the form WIDTHxHEIGHT e.g. '640x480' or '85x85'. If
|
1796
|
* an image toolkit is installed the image will be resized down to these
|
1797
|
* dimensions. A value of 0 indicates no restriction on size, so resizing
|
1798
|
* will be attempted.
|
1799
|
* @param $minimum_dimensions
|
1800
|
* An optional string in the form WIDTHxHEIGHT. This will check that the
|
1801
|
* image meets a minimum size. A value of 0 indicates no restriction.
|
1802
|
*
|
1803
|
* @return
|
1804
|
* An array. If the file is an image and did not meet the requirements, it
|
1805
|
* will contain an error message.
|
1806
|
*
|
1807
|
* @see hook_file_validate()
|
1808
|
*/
|
1809
|
function file_validate_image_resolution(stdClass $file, $maximum_dimensions = 0, $minimum_dimensions = 0) {
|
1810
|
$errors = array();
|
1811
|
|
1812
|
// Check first that the file is an image.
|
1813
|
if ($info = image_get_info($file->uri)) {
|
1814
|
if ($maximum_dimensions) {
|
1815
|
// Check that it is smaller than the given dimensions.
|
1816
|
list($width, $height) = explode('x', $maximum_dimensions);
|
1817
|
if ($info['width'] > $width || $info['height'] > $height) {
|
1818
|
// Try to resize the image to fit the dimensions.
|
1819
|
if ($image = image_load($file->uri)) {
|
1820
|
image_scale($image, $width, $height);
|
1821
|
image_save($image);
|
1822
|
$file->filesize = $image->info['file_size'];
|
1823
|
drupal_set_message(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', array('%dimensions' => $maximum_dimensions)));
|
1824
|
}
|
1825
|
else {
|
1826
|
$errors[] = t('The image is too large; the maximum dimensions are %dimensions pixels.', array('%dimensions' => $maximum_dimensions));
|
1827
|
}
|
1828
|
}
|
1829
|
}
|
1830
|
|
1831
|
if ($minimum_dimensions) {
|
1832
|
// Check that it is larger than the given dimensions.
|
1833
|
list($width, $height) = explode('x', $minimum_dimensions);
|
1834
|
if ($info['width'] < $width || $info['height'] < $height) {
|
1835
|
$errors[] = t('The image is too small; the minimum dimensions are %dimensions pixels.', array('%dimensions' => $minimum_dimensions));
|
1836
|
}
|
1837
|
}
|
1838
|
}
|
1839
|
|
1840
|
return $errors;
|
1841
|
}
|
1842
|
|
1843
|
/**
|
1844
|
* Saves a file to the specified destination and creates a database entry.
|
1845
|
*
|
1846
|
* @param $data
|
1847
|
* A string containing the contents of the file.
|
1848
|
* @param $destination
|
1849
|
* A string containing the destination URI. This must be a stream wrapper URI.
|
1850
|
* If no value is provided, a randomized name will be generated and the file
|
1851
|
* will be saved using Drupal's default files scheme, usually "public://".
|
1852
|
* @param $replace
|
1853
|
* Replace behavior when the destination file already exists:
|
1854
|
* - FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with
|
1855
|
* the destination name exists then its database entry will be updated. If
|
1856
|
* no database entry is found then a new one will be created.
|
1857
|
* - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
|
1858
|
* unique.
|
1859
|
* - FILE_EXISTS_ERROR - Do nothing and return FALSE.
|
1860
|
*
|
1861
|
* @return
|
1862
|
* A file object, or FALSE on error.
|
1863
|
*
|
1864
|
* @see file_unmanaged_save_data()
|
1865
|
*/
|
1866
|
function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
|
1867
|
global $user;
|
1868
|
|
1869
|
if (empty($destination)) {
|
1870
|
$destination = file_default_scheme() . '://';
|
1871
|
}
|
1872
|
if (!file_valid_uri($destination)) {
|
1873
|
watchdog('file', 'The data could not be saved because the destination %destination is invalid. This may be caused by improper use of file_save_data() or a missing stream wrapper.', array('%destination' => $destination));
|
1874
|
drupal_set_message(t('The data could not be saved, because the destination is invalid. More information is available in the system log.'), 'error');
|
1875
|
return FALSE;
|
1876
|
}
|
1877
|
|
1878
|
if ($uri = file_unmanaged_save_data($data, $destination, $replace)) {
|
1879
|
// Create a file object.
|
1880
|
$file = new stdClass();
|
1881
|
$file->fid = NULL;
|
1882
|
$file->uri = $uri;
|
1883
|
$file->filename = drupal_basename($uri);
|
1884
|
$file->filemime = file_get_mimetype($file->uri);
|
1885
|
$file->uid = $user->uid;
|
1886
|
$file->status = FILE_STATUS_PERMANENT;
|
1887
|
// If we are replacing an existing file re-use its database record.
|
1888
|
if ($replace == FILE_EXISTS_REPLACE) {
|
1889
|
$existing_files = file_load_multiple(array(), array('uri' => $uri));
|
1890
|
if (count($existing_files)) {
|
1891
|
$existing = reset($existing_files);
|
1892
|
$file->fid = $existing->fid;
|
1893
|
$file->filename = $existing->filename;
|
1894
|
}
|
1895
|
}
|
1896
|
// If we are renaming around an existing file (rather than a directory),
|
1897
|
// use its basename for the filename.
|
1898
|
elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
|
1899
|
$file->filename = drupal_basename($destination);
|
1900
|
}
|
1901
|
|
1902
|
return file_save($file);
|
1903
|
}
|
1904
|
return FALSE;
|
1905
|
}
|
1906
|
|
1907
|
/**
|
1908
|
* Saves a string to the specified destination without invoking file API.
|
1909
|
*
|
1910
|
* This function is identical to file_save_data() except the file will not be
|
1911
|
* saved to the {file_managed} table and none of the file_* hooks will be
|
1912
|
* called.
|
1913
|
*
|
1914
|
* @param $data
|
1915
|
* A string containing the contents of the file.
|
1916
|
* @param $destination
|
1917
|
* A string containing the destination location. This must be a stream wrapper
|
1918
|
* URI. If no value is provided, a randomized name will be generated and the
|
1919
|
* file will be saved using Drupal's default files scheme, usually
|
1920
|
* "public://".
|
1921
|
* @param $replace
|
1922
|
* Replace behavior when the destination file already exists:
|
1923
|
* - FILE_EXISTS_REPLACE - Replace the existing file.
|
1924
|
* - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
|
1925
|
* unique.
|
1926
|
* - FILE_EXISTS_ERROR - Do nothing and return FALSE.
|
1927
|
*
|
1928
|
* @return
|
1929
|
* A string with the path of the resulting file, or FALSE on error.
|
1930
|
*
|
1931
|
* @see file_save_data()
|
1932
|
*/
|
1933
|
function file_unmanaged_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
|
1934
|
// Write the data to a temporary file.
|
1935
|
$temp_name = drupal_tempnam('temporary://', 'file');
|
1936
|
if (file_put_contents($temp_name, $data) === FALSE) {
|
1937
|
drupal_set_message(t('The file could not be created.'), 'error');
|
1938
|
return FALSE;
|
1939
|
}
|
1940
|
|
1941
|
// Move the file to its final destination.
|
1942
|
return file_unmanaged_move($temp_name, $destination, $replace);
|
1943
|
}
|
1944
|
|
1945
|
/**
|
1946
|
* Transfers a file to the client using HTTP.
|
1947
|
*
|
1948
|
* Pipes a file through Drupal to the client.
|
1949
|
*
|
1950
|
* @param $uri
|
1951
|
* String specifying the file URI to transfer.
|
1952
|
* @param $headers
|
1953
|
* An array of HTTP headers to send along with file.
|
1954
|
*/
|
1955
|
function file_transfer($uri, $headers) {
|
1956
|
if (ob_get_level()) {
|
1957
|
ob_end_clean();
|
1958
|
}
|
1959
|
|
1960
|
foreach ($headers as $name => $value) {
|
1961
|
drupal_add_http_header($name, $value);
|
1962
|
}
|
1963
|
drupal_send_headers();
|
1964
|
$scheme = file_uri_scheme($uri);
|
1965
|
// Transfer file in 1024 byte chunks to save memory usage.
|
1966
|
if ($scheme && file_stream_wrapper_valid_scheme($scheme) && $fd = fopen($uri, 'rb')) {
|
1967
|
while (!feof($fd)) {
|
1968
|
print fread($fd, 1024);
|
1969
|
}
|
1970
|
fclose($fd);
|
1971
|
}
|
1972
|
else {
|
1973
|
drupal_not_found();
|
1974
|
}
|
1975
|
drupal_exit();
|
1976
|
}
|
1977
|
|
1978
|
/**
|
1979
|
* Menu handler for private file transfers.
|
1980
|
*
|
1981
|
* Call modules that implement hook_file_download() to find out if a file is
|
1982
|
* accessible and what headers it should be transferred with. If one or more
|
1983
|
* modules returned headers the download will start with the returned headers.
|
1984
|
* If a module returns -1 drupal_access_denied() will be returned. If the file
|
1985
|
* exists but no modules responded drupal_access_denied() will be returned.
|
1986
|
* If the file does not exist drupal_not_found() will be returned.
|
1987
|
*
|
1988
|
* @see system_menu()
|
1989
|
*/
|
1990
|
function file_download() {
|
1991
|
// Merge remainder of arguments from GET['q'], into relative file path.
|
1992
|
$args = func_get_args();
|
1993
|
$scheme = array_shift($args);
|
1994
|
$target = implode('/', $args);
|
1995
|
$uri = $scheme . '://' . $target;
|
1996
|
if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) {
|
1997
|
$headers = file_download_headers($uri);
|
1998
|
if (count($headers)) {
|
1999
|
file_transfer($uri, $headers);
|
2000
|
}
|
2001
|
drupal_access_denied();
|
2002
|
}
|
2003
|
else {
|
2004
|
drupal_not_found();
|
2005
|
}
|
2006
|
drupal_exit();
|
2007
|
}
|
2008
|
|
2009
|
/**
|
2010
|
* Retrieves headers for a private file download.
|
2011
|
*
|
2012
|
* Calls all module implementations of hook_file_download() to retrieve headers
|
2013
|
* for files by the module that originally provided the file. The presence of
|
2014
|
* returned headers indicates the current user has access to the file.
|
2015
|
*
|
2016
|
* @param $uri
|
2017
|
* The URI for the file whose headers should be retrieved.
|
2018
|
*
|
2019
|
* @return
|
2020
|
* If access is allowed, headers for the file, suitable for passing to
|
2021
|
* file_transfer(). If access is not allowed, an empty array will be returned.
|
2022
|
*
|
2023
|
* @see file_transfer()
|
2024
|
* @see file_download_access()
|
2025
|
* @see hook_file_downlaod()
|
2026
|
*/
|
2027
|
function file_download_headers($uri) {
|
2028
|
// Let other modules provide headers and control access to the file.
|
2029
|
// module_invoke_all() uses array_merge_recursive() which merges header
|
2030
|
// values into a new array. To avoid that and allow modules to override
|
2031
|
// headers instead, use array_merge() to merge the returned arrays.
|
2032
|
$headers = array();
|
2033
|
foreach (module_implements('file_download') as $module) {
|
2034
|
$function = $module . '_file_download';
|
2035
|
$result = $function($uri);
|
2036
|
if ($result == -1) {
|
2037
|
// Throw away the headers received so far.
|
2038
|
$headers = array();
|
2039
|
break;
|
2040
|
}
|
2041
|
if (isset($result) && is_array($result)) {
|
2042
|
$headers = array_merge($headers, $result);
|
2043
|
}
|
2044
|
}
|
2045
|
return $headers;
|
2046
|
}
|
2047
|
|
2048
|
/**
|
2049
|
* Checks that the current user has access to a particular file.
|
2050
|
*
|
2051
|
* The return value of this function hinges on the return value from
|
2052
|
* file_download_headers(), which is the function responsible for collecting
|
2053
|
* access information through hook_file_download().
|
2054
|
*
|
2055
|
* If immediately transferring the file to the browser and the headers will
|
2056
|
* need to be retrieved, the return value of file_download_headers() should be
|
2057
|
* used to determine access directly, so that access checks will not be run
|
2058
|
* twice.
|
2059
|
*
|
2060
|
* @param $uri
|
2061
|
* The URI for the file whose access should be retrieved.
|
2062
|
*
|
2063
|
* @return
|
2064
|
* Boolean TRUE if access is allowed. FALSE if access is not allowed.
|
2065
|
*
|
2066
|
* @see file_download_headers()
|
2067
|
* @see hook_file_download()
|
2068
|
*/
|
2069
|
function file_download_access($uri) {
|
2070
|
return count(file_download_headers($uri)) > 0;
|
2071
|
}
|
2072
|
|
2073
|
/**
|
2074
|
* Finds all files that match a given mask in a given directory.
|
2075
|
*
|
2076
|
* Directories and files beginning with a period are excluded; this
|
2077
|
* prevents hidden files and directories (such as SVN working directories)
|
2078
|
* from being scanned.
|
2079
|
*
|
2080
|
* @param $dir
|
2081
|
* The base directory or URI to scan, without trailing slash.
|
2082
|
* @param $mask
|
2083
|
* The preg_match() regular expression of the files to find.
|
2084
|
* @param $options
|
2085
|
* An associative array of additional options, with the following elements:
|
2086
|
* - 'nomask': The preg_match() regular expression of the files to ignore.
|
2087
|
* Defaults to '/(\.\.?|CVS)$/'.
|
2088
|
* - 'callback': The callback function to call for each match. There is no
|
2089
|
* default callback.
|
2090
|
* - 'recurse': When TRUE, the directory scan will recurse the entire tree
|
2091
|
* starting at the provided directory. Defaults to TRUE.
|
2092
|
* - 'key': The key to be used for the returned associative array of files.
|
2093
|
* Possible values are 'uri', for the file's URI; 'filename', for the
|
2094
|
* basename of the file; and 'name' for the name of the file without the
|
2095
|
* extension. Defaults to 'uri'.
|
2096
|
* - 'min_depth': Minimum depth of directories to return files from. Defaults
|
2097
|
* to 0.
|
2098
|
* @param $depth
|
2099
|
* Current depth of recursion. This parameter is only used internally and
|
2100
|
* should not be passed in.
|
2101
|
*
|
2102
|
* @return
|
2103
|
* An associative array (keyed on the chosen key) of objects with 'uri',
|
2104
|
* 'filename', and 'name' members corresponding to the matching files.
|
2105
|
*/
|
2106
|
function file_scan_directory($dir, $mask, $options = array(), $depth = 0) {
|
2107
|
// Merge in defaults.
|
2108
|
$options += array(
|
2109
|
'nomask' => '/(\.\.?|CVS)$/',
|
2110
|
'callback' => 0,
|
2111
|
'recurse' => TRUE,
|
2112
|
'key' => 'uri',
|
2113
|
'min_depth' => 0,
|
2114
|
);
|
2115
|
|
2116
|
$options['key'] = in_array($options['key'], array('uri', 'filename', 'name')) ? $options['key'] : 'uri';
|
2117
|
$files = array();
|
2118
|
if (is_dir($dir) && $handle = opendir($dir)) {
|
2119
|
while (FALSE !== ($filename = readdir($handle))) {
|
2120
|
if (!preg_match($options['nomask'], $filename) && $filename[0] != '.') {
|
2121
|
$uri = "$dir/$filename";
|
2122
|
$uri = file_stream_wrapper_uri_normalize($uri);
|
2123
|
if (is_dir($uri) && $options['recurse']) {
|
2124
|
// Give priority to files in this folder by merging them in after any subdirectory files.
|
2125
|
$files = array_merge(file_scan_directory($uri, $mask, $options, $depth + 1), $files);
|
2126
|
}
|
2127
|
elseif ($depth >= $options['min_depth'] && preg_match($mask, $filename)) {
|
2128
|
// Always use this match over anything already set in $files with the
|
2129
|
// same $$options['key'].
|
2130
|
$file = new stdClass();
|
2131
|
$file->uri = $uri;
|
2132
|
$file->filename = $filename;
|
2133
|
$file->name = pathinfo($filename, PATHINFO_FILENAME);
|
2134
|
$key = $options['key'];
|
2135
|
$files[$file->$key] = $file;
|
2136
|
if ($options['callback']) {
|
2137
|
$options['callback']($uri);
|
2138
|
}
|
2139
|
}
|
2140
|
}
|
2141
|
}
|
2142
|
|
2143
|
closedir($handle);
|
2144
|
}
|
2145
|
|
2146
|
return $files;
|
2147
|
}
|
2148
|
|
2149
|
/**
|
2150
|
* Determines the maximum file upload size by querying the PHP settings.
|
2151
|
*
|
2152
|
* @return
|
2153
|
* A file size limit in bytes based on the PHP upload_max_filesize and
|
2154
|
* post_max_size
|
2155
|
*/
|
2156
|
function file_upload_max_size() {
|
2157
|
static $max_size = -1;
|
2158
|
|
2159
|
if ($max_size < 0) {
|
2160
|
// Start with post_max_size.
|
2161
|
$max_size = parse_size(ini_get('post_max_size'));
|
2162
|
|
2163
|
// If upload_max_size is less, then reduce. Except if upload_max_size is
|
2164
|
// zero, which indicates no limit.
|
2165
|
$upload_max = parse_size(ini_get('upload_max_filesize'));
|
2166
|
if ($upload_max > 0 && $upload_max < $max_size) {
|
2167
|
$max_size = $upload_max;
|
2168
|
}
|
2169
|
}
|
2170
|
return $max_size;
|
2171
|
}
|
2172
|
|
2173
|
/**
|
2174
|
* Determines an Internet Media Type or MIME type from a filename.
|
2175
|
*
|
2176
|
* @param $uri
|
2177
|
* A string containing the URI, path, or filename.
|
2178
|
* @param $mapping
|
2179
|
* An optional map of extensions to their mimetypes, in the form:
|
2180
|
* - 'mimetypes': a list of mimetypes, keyed by an identifier,
|
2181
|
* - 'extensions': the mapping itself, an associative array in which
|
2182
|
* the key is the extension (lowercase) and the value is the mimetype
|
2183
|
* identifier. If $mapping is NULL file_mimetype_mapping() is called.
|
2184
|
*
|
2185
|
* @return
|
2186
|
* The internet media type registered for the extension or
|
2187
|
* application/octet-stream for unknown extensions.
|
2188
|
*
|
2189
|
* @see file_default_mimetype_mapping()
|
2190
|
*/
|
2191
|
function file_get_mimetype($uri, $mapping = NULL) {
|
2192
|
if ($wrapper = file_stream_wrapper_get_instance_by_uri($uri)) {
|
2193
|
return $wrapper->getMimeType($uri, $mapping);
|
2194
|
}
|
2195
|
else {
|
2196
|
// getMimeType() is not implementation specific, so we can directly
|
2197
|
// call it without an instance.
|
2198
|
return DrupalLocalStreamWrapper::getMimeType($uri, $mapping);
|
2199
|
}
|
2200
|
}
|
2201
|
|
2202
|
/**
|
2203
|
* Sets the permissions on a file or directory.
|
2204
|
*
|
2205
|
* This function will use the 'file_chmod_directory' and 'file_chmod_file'
|
2206
|
* variables for the default modes for directories and uploaded/generated
|
2207
|
* files. By default these will give everyone read access so that users
|
2208
|
* accessing the files with a user account without the webserver group (e.g.
|
2209
|
* via FTP) can read these files, and give group write permissions so webserver
|
2210
|
* group members (e.g. a vhost account) can alter files uploaded and owned by
|
2211
|
* the webserver.
|
2212
|
*
|
2213
|
* PHP's chmod does not support stream wrappers so we use our wrapper
|
2214
|
* implementation which interfaces with chmod() by default. Contrib wrappers
|
2215
|
* may override this behavior in their implementations as needed.
|
2216
|
*
|
2217
|
* @param $uri
|
2218
|
* A string containing a URI file, or directory path.
|
2219
|
* @param $mode
|
2220
|
* Integer value for the permissions. Consult PHP chmod() documentation for
|
2221
|
* more information.
|
2222
|
*
|
2223
|
* @return
|
2224
|
* TRUE for success, FALSE in the event of an error.
|
2225
|
*
|
2226
|
* @ingroup php_wrappers
|
2227
|
*/
|
2228
|
function drupal_chmod($uri, $mode = NULL) {
|
2229
|
if (!isset($mode)) {
|
2230
|
if (is_dir($uri)) {
|
2231
|
$mode = variable_get('file_chmod_directory', 0775);
|
2232
|
}
|
2233
|
else {
|
2234
|
$mode = variable_get('file_chmod_file', 0664);
|
2235
|
}
|
2236
|
}
|
2237
|
|
2238
|
// If this URI is a stream, pass it off to the appropriate stream wrapper.
|
2239
|
// Otherwise, attempt PHP's chmod. This allows use of drupal_chmod even
|
2240
|
// for unmanaged files outside of the stream wrapper interface.
|
2241
|
if ($wrapper = file_stream_wrapper_get_instance_by_uri($uri)) {
|
2242
|
if ($wrapper->chmod($mode)) {
|
2243
|
return TRUE;
|
2244
|
}
|
2245
|
}
|
2246
|
else {
|
2247
|
if (@chmod($uri, $mode)) {
|
2248
|
return TRUE;
|
2249
|
}
|
2250
|
}
|
2251
|
|
2252
|
watchdog('file', 'The file permissions could not be set on %uri.', array('%uri' => $uri), WATCHDOG_ERROR);
|
2253
|
return FALSE;
|
2254
|
}
|
2255
|
|
2256
|
/**
|
2257
|
* Deletes a file.
|
2258
|
*
|
2259
|
* PHP's unlink() is broken on Windows, as it can fail to remove a file
|
2260
|
* when it has a read-only flag set.
|
2261
|
*
|
2262
|
* @param $uri
|
2263
|
* A URI or pathname.
|
2264
|
* @param $context
|
2265
|
* Refer to http://php.net/manual/ref.stream.php
|
2266
|
*
|
2267
|
* @return
|
2268
|
* Boolean TRUE on success, or FALSE on failure.
|
2269
|
*
|
2270
|
* @see unlink()
|
2271
|
* @ingroup php_wrappers
|
2272
|
*/
|
2273
|
function drupal_unlink($uri, $context = NULL) {
|
2274
|
$scheme = file_uri_scheme($uri);
|
2275
|
if ((!$scheme || !file_stream_wrapper_valid_scheme($scheme)) && (substr(PHP_OS, 0, 3) == 'WIN')) {
|
2276
|
chmod($uri, 0600);
|
2277
|
}
|
2278
|
if ($context) {
|
2279
|
return unlink($uri, $context);
|
2280
|
}
|
2281
|
else {
|
2282
|
return unlink($uri);
|
2283
|
}
|
2284
|
}
|
2285
|
|
2286
|
/**
|
2287
|
* Resolves the absolute filepath of a local URI or filepath.
|
2288
|
*
|
2289
|
* The use of drupal_realpath() is discouraged, because it does not work for
|
2290
|
* remote URIs. Except in rare cases, URIs should not be manually resolved.
|
2291
|
*
|
2292
|
* Only use this function if you know that the stream wrapper in the URI uses
|
2293
|
* the local file system, and you need to pass an absolute path to a function
|
2294
|
* that is incompatible with stream URIs.
|
2295
|
*
|
2296
|
* @param string $uri
|
2297
|
* A stream wrapper URI or a filepath, possibly including one or more symbolic
|
2298
|
* links.
|
2299
|
*
|
2300
|
* @return string|false
|
2301
|
* The absolute local filepath (with no symbolic links), or FALSE on failure.
|
2302
|
*
|
2303
|
* @see DrupalStreamWrapperInterface::realpath()
|
2304
|
* @see http://php.net/manual/function.realpath.php
|
2305
|
* @ingroup php_wrappers
|
2306
|
*/
|
2307
|
function drupal_realpath($uri) {
|
2308
|
// If this URI is a stream, pass it off to the appropriate stream wrapper.
|
2309
|
// Otherwise, attempt PHP's realpath. This allows use of drupal_realpath even
|
2310
|
// for unmanaged files outside of the stream wrapper interface.
|
2311
|
if ($wrapper = file_stream_wrapper_get_instance_by_uri($uri)) {
|
2312
|
return $wrapper->realpath();
|
2313
|
}
|
2314
|
// Check that the URI has a value. There is a bug in PHP 5.2 on *BSD systems
|
2315
|
// that makes realpath not return FALSE as expected when passing an empty
|
2316
|
// variable.
|
2317
|
// @todo Remove when Drupal drops support for PHP 5.2.
|
2318
|
elseif (!empty($uri)) {
|
2319
|
return realpath($uri);
|
2320
|
}
|
2321
|
return FALSE;
|
2322
|
}
|
2323
|
|
2324
|
/**
|
2325
|
* Gets the name of the directory from a given path.
|
2326
|
*
|
2327
|
* PHP's dirname() does not properly pass streams, so this function fills
|
2328
|
* that gap. It is backwards compatible with normal paths and will use
|
2329
|
* PHP's dirname() as a fallback.
|
2330
|
*
|
2331
|
* Compatibility: normal paths and stream wrappers.
|
2332
|
*
|
2333
|
* @param $uri
|
2334
|
* A URI or path.
|
2335
|
*
|
2336
|
* @return
|
2337
|
* A string containing the directory name.
|
2338
|
*
|
2339
|
* @see dirname()
|
2340
|
* @see http://drupal.org/node/515192
|
2341
|
* @ingroup php_wrappers
|
2342
|
*/
|
2343
|
function drupal_dirname($uri) {
|
2344
|
$scheme = file_uri_scheme($uri);
|
2345
|
|
2346
|
if ($scheme && file_stream_wrapper_valid_scheme($scheme)) {
|
2347
|
return file_stream_wrapper_get_instance_by_scheme($scheme)->dirname($uri);
|
2348
|
}
|
2349
|
else {
|
2350
|
return dirname($uri);
|
2351
|
}
|
2352
|
}
|
2353
|
|
2354
|
/**
|
2355
|
* Gets the filename from a given path.
|
2356
|
*
|
2357
|
* PHP's basename() does not properly support streams or filenames beginning
|
2358
|
* with a non-US-ASCII character.
|
2359
|
*
|
2360
|
* @see http://bugs.php.net/bug.php?id=37738
|
2361
|
* @see basename()
|
2362
|
*
|
2363
|
* @ingroup php_wrappers
|
2364
|
*/
|
2365
|
function drupal_basename($uri, $suffix = NULL) {
|
2366
|
$separators = '/';
|
2367
|
if (DIRECTORY_SEPARATOR != '/') {
|
2368
|
// For Windows OS add special separator.
|
2369
|
$separators .= DIRECTORY_SEPARATOR;
|
2370
|
}
|
2371
|
// Remove right-most slashes when $uri points to directory.
|
2372
|
$uri = rtrim($uri, $separators);
|
2373
|
// Returns the trailing part of the $uri starting after one of the directory
|
2374
|
// separators.
|
2375
|
$filename = preg_match('@[^' . preg_quote($separators, '@') . ']+$@', $uri, $matches) ? $matches[0] : '';
|
2376
|
// Cuts off a suffix from the filename.
|
2377
|
if ($suffix) {
|
2378
|
$filename = preg_replace('@' . preg_quote($suffix, '@') . '$@', '', $filename);
|
2379
|
}
|
2380
|
return $filename;
|
2381
|
}
|
2382
|
|
2383
|
/**
|
2384
|
* Creates a directory using Drupal's default mode.
|
2385
|
*
|
2386
|
* PHP's mkdir() does not respect Drupal's default permissions mode. If a mode
|
2387
|
* is not provided, this function will make sure that Drupal's is used.
|
2388
|
*
|
2389
|
* Compatibility: normal paths and stream wrappers.
|
2390
|
*
|
2391
|
* @param $uri
|
2392
|
* A URI or pathname.
|
2393
|
* @param $mode
|
2394
|
* By default the Drupal mode is used.
|
2395
|
* @param $recursive
|
2396
|
* Default to FALSE.
|
2397
|
* @param $context
|
2398
|
* Refer to http://php.net/manual/ref.stream.php
|
2399
|
*
|
2400
|
* @return
|
2401
|
* Boolean TRUE on success, or FALSE on failure.
|
2402
|
*
|
2403
|
* @see mkdir()
|
2404
|
* @see http://drupal.org/node/515192
|
2405
|
* @ingroup php_wrappers
|
2406
|
*/
|
2407
|
function drupal_mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) {
|
2408
|
if (!isset($mode)) {
|
2409
|
$mode = variable_get('file_chmod_directory', 0775);
|
2410
|
}
|
2411
|
|
2412
|
if (!isset($context)) {
|
2413
|
return mkdir($uri, $mode, $recursive);
|
2414
|
}
|
2415
|
else {
|
2416
|
return mkdir($uri, $mode, $recursive, $context);
|
2417
|
}
|
2418
|
}
|
2419
|
|
2420
|
/**
|
2421
|
* Removes a directory.
|
2422
|
*
|
2423
|
* PHP's rmdir() is broken on Windows, as it can fail to remove a directory
|
2424
|
* when it has a read-only flag set.
|
2425
|
*
|
2426
|
* @param $uri
|
2427
|
* A URI or pathname.
|
2428
|
* @param $context
|
2429
|
* Refer to http://php.net/manual/ref.stream.php
|
2430
|
*
|
2431
|
* @return
|
2432
|
* Boolean TRUE on success, or FALSE on failure.
|
2433
|
*
|
2434
|
* @see rmdir()
|
2435
|
* @ingroup php_wrappers
|
2436
|
*/
|
2437
|
function drupal_rmdir($uri, $context = NULL) {
|
2438
|
$scheme = file_uri_scheme($uri);
|
2439
|
if ((!$scheme || !file_stream_wrapper_valid_scheme($scheme)) && (substr(PHP_OS, 0, 3) == 'WIN')) {
|
2440
|
chmod($uri, 0700);
|
2441
|
}
|
2442
|
if ($context) {
|
2443
|
return rmdir($uri, $context);
|
2444
|
}
|
2445
|
else {
|
2446
|
return rmdir($uri);
|
2447
|
}
|
2448
|
}
|
2449
|
|
2450
|
/**
|
2451
|
* Creates a file with a unique filename in the specified directory.
|
2452
|
*
|
2453
|
* PHP's tempnam() does not return a URI like we want. This function
|
2454
|
* will return a URI if given a URI, or it will return a filepath if
|
2455
|
* given a filepath.
|
2456
|
*
|
2457
|
* Compatibility: normal paths and stream wrappers.
|
2458
|
*
|
2459
|
* @param $directory
|
2460
|
* The directory where the temporary filename will be created.
|
2461
|
* @param $prefix
|
2462
|
* The prefix of the generated temporary filename.
|
2463
|
* Note: Windows uses only the first three characters of prefix.
|
2464
|
*
|
2465
|
* @return
|
2466
|
* The new temporary filename, or FALSE on failure.
|
2467
|
*
|
2468
|
* @see tempnam()
|
2469
|
* @see http://drupal.org/node/515192
|
2470
|
* @ingroup php_wrappers
|
2471
|
*/
|
2472
|
function drupal_tempnam($directory, $prefix) {
|
2473
|
$scheme = file_uri_scheme($directory);
|
2474
|
|
2475
|
if ($scheme && file_stream_wrapper_valid_scheme($scheme)) {
|
2476
|
$wrapper = file_stream_wrapper_get_instance_by_scheme($scheme);
|
2477
|
|
2478
|
if ($filename = tempnam($wrapper->getDirectoryPath(), $prefix)) {
|
2479
|
return $scheme . '://' . drupal_basename($filename);
|
2480
|
}
|
2481
|
else {
|
2482
|
return FALSE;
|
2483
|
}
|
2484
|
}
|
2485
|
else {
|
2486
|
// Handle as a normal tempnam() call.
|
2487
|
return tempnam($directory, $prefix);
|
2488
|
}
|
2489
|
}
|
2490
|
|
2491
|
/**
|
2492
|
* Gets the path of system-appropriate temporary directory.
|
2493
|
*/
|
2494
|
function file_directory_temp() {
|
2495
|
$temporary_directory = variable_get('file_temporary_path', NULL);
|
2496
|
|
2497
|
if (empty($temporary_directory)) {
|
2498
|
$directories = array();
|
2499
|
|
2500
|
// Has PHP been set with an upload_tmp_dir?
|
2501
|
if (ini_get('upload_tmp_dir')) {
|
2502
|
$directories[] = ini_get('upload_tmp_dir');
|
2503
|
}
|
2504
|
|
2505
|
// Operating system specific dirs.
|
2506
|
if (substr(PHP_OS, 0, 3) == 'WIN') {
|
2507
|
$directories[] = 'c:\\windows\\temp';
|
2508
|
$directories[] = 'c:\\winnt\\temp';
|
2509
|
}
|
2510
|
else {
|
2511
|
$directories[] = '/tmp';
|
2512
|
}
|
2513
|
// PHP may be able to find an alternative tmp directory.
|
2514
|
// This function exists in PHP 5 >= 5.2.1, but Drupal
|
2515
|
// requires PHP 5 >= 5.2.0, so we check for it.
|
2516
|
if (function_exists('sys_get_temp_dir')) {
|
2517
|
$directories[] = sys_get_temp_dir();
|
2518
|
}
|
2519
|
|
2520
|
foreach ($directories as $directory) {
|
2521
|
if (is_dir($directory) && is_writable($directory)) {
|
2522
|
$temporary_directory = $directory;
|
2523
|
break;
|
2524
|
}
|
2525
|
}
|
2526
|
|
2527
|
if (empty($temporary_directory)) {
|
2528
|
// If no directory has been found default to 'files/tmp'.
|
2529
|
$temporary_directory = variable_get('file_public_path', conf_path() . '/files') . '/tmp';
|
2530
|
|
2531
|
// Windows accepts paths with either slash (/) or backslash (\), but will
|
2532
|
// not accept a path which contains both a slash and a backslash. Since
|
2533
|
// the 'file_public_path' variable may have either format, we sanitize
|
2534
|
// everything to use slash which is supported on all platforms.
|
2535
|
$temporary_directory = str_replace('\\', '/', $temporary_directory);
|
2536
|
}
|
2537
|
// Save the path of the discovered directory.
|
2538
|
variable_set('file_temporary_path', $temporary_directory);
|
2539
|
}
|
2540
|
|
2541
|
return $temporary_directory;
|
2542
|
}
|
2543
|
|
2544
|
/**
|
2545
|
* Examines a file object and returns appropriate content headers for download.
|
2546
|
*
|
2547
|
* @param $file
|
2548
|
* A file object.
|
2549
|
*
|
2550
|
* @return
|
2551
|
* An associative array of headers, as expected by file_transfer().
|
2552
|
*/
|
2553
|
function file_get_content_headers($file) {
|
2554
|
$name = mime_header_encode($file->filename);
|
2555
|
$type = mime_header_encode($file->filemime);
|
2556
|
|
2557
|
return array(
|
2558
|
'Content-Type' => $type,
|
2559
|
'Content-Length' => $file->filesize,
|
2560
|
'Cache-Control' => 'private',
|
2561
|
);
|
2562
|
}
|
2563
|
|
2564
|
/**
|
2565
|
* @} End of "defgroup file".
|
2566
|
*/
|