1 | <?php |
---|
2 | /** |
---|
3 | * @file |
---|
4 | * Common functionality for file handling CCK field modules. |
---|
5 | */ |
---|
6 | |
---|
7 | /** |
---|
8 | * Load a file from the database. |
---|
9 | * |
---|
10 | * @param $fid |
---|
11 | * A numeric file id or string containing the file path. |
---|
12 | * @param $reset |
---|
13 | * Whether to reset the internal file_load cache. |
---|
14 | * @return |
---|
15 | * A file array. |
---|
16 | */ |
---|
17 | function field_file_load($fid, $reset = NULL) { |
---|
18 | // Reset internal cache. |
---|
19 | if (isset($reset)) { |
---|
20 | _field_file_cache(NULL, TRUE); |
---|
21 | } |
---|
22 | |
---|
23 | if (empty($fid)) { |
---|
24 | return array('fid' => 0, 'filepath' => '', 'filename' => '', 'filemime' => '', 'filesize' => 0); |
---|
25 | } |
---|
26 | |
---|
27 | $files = _field_file_cache(); |
---|
28 | |
---|
29 | // Serve file from internal cache if available. |
---|
30 | if (empty($files[$fid])) { |
---|
31 | if (is_numeric($fid)) { |
---|
32 | $file = db_fetch_object(db_query('SELECT f.* FROM {files} f WHERE f.fid = %d', $fid)); |
---|
33 | } |
---|
34 | else { |
---|
35 | $file = db_fetch_object(db_query("SELECT f.* FROM {files} f WHERE f.filepath = '%s'", $fid)); |
---|
36 | } |
---|
37 | |
---|
38 | if (!$file) { |
---|
39 | $file = (object) array('fid' => 0, 'filepath' => '', 'filename' => '', 'filemime' => '', 'filesize' => 0); |
---|
40 | } |
---|
41 | |
---|
42 | foreach (module_implements('file_load') as $module) { |
---|
43 | if ($module != 'field') { |
---|
44 | $function = $module .'_file_load'; |
---|
45 | $function($file); |
---|
46 | } |
---|
47 | } |
---|
48 | |
---|
49 | // Cache the fully loaded file for later use. |
---|
50 | $files = _field_file_cache($file); |
---|
51 | } |
---|
52 | |
---|
53 | // Cast to an array for the field storage. |
---|
54 | // Contrary to fields, hook_file() and core file functions expect objects. |
---|
55 | return isset($files[$fid]) ? (array) $files[$fid] : FALSE; |
---|
56 | } |
---|
57 | |
---|
58 | /** |
---|
59 | * Save a file upload to a new location. |
---|
60 | * The source file is validated as a proper upload and handled as such. By |
---|
61 | * implementing hook_file($op = 'insert'), modules are able to act on the file |
---|
62 | * upload and to add their own properties to the file. |
---|
63 | * |
---|
64 | * The file will be added to the files table as a temporary file. Temporary |
---|
65 | * files are periodically cleaned. To make the file permanent file call |
---|
66 | * file_set_status() to change its status. |
---|
67 | * |
---|
68 | * @param $source |
---|
69 | * A string specifying the name of the upload field to save. |
---|
70 | * @param $validators |
---|
71 | * An optional, associative array of callback functions used to validate the |
---|
72 | * file. The keys are function names and the values arrays of callback |
---|
73 | * parameters which will be passed in after the user and file objects. The |
---|
74 | * functions should return an array of error messages, an empty array |
---|
75 | * indicates that the file passed validation. The functions will be called in |
---|
76 | * the order specified. |
---|
77 | * @param $dest |
---|
78 | * A string containing the directory $source should be copied to. If this is |
---|
79 | * not provided or is not writable, the temporary directory will be used. |
---|
80 | * @return |
---|
81 | * An array containing the file information, or 0 in the event of an error. |
---|
82 | */ |
---|
83 | function field_file_save_upload($source, $validators = array(), $dest = FALSE) { |
---|
84 | if (!$file = file_save_upload($source, $validators, $dest, FILE_EXISTS_RENAME)) { |
---|
85 | return 0; |
---|
86 | } |
---|
87 | if (!@chmod($file->filepath, 0664)) { |
---|
88 | watchdog('filefield', 'Could not set permissions on destination file: %file', array('%file' => $file->filepath)); |
---|
89 | } |
---|
90 | |
---|
91 | // Let modules add additional properties to the yet barebone file object. |
---|
92 | foreach (module_implements('file_insert') as $module) { |
---|
93 | $function = $module .'_file_insert'; |
---|
94 | $function($file); |
---|
95 | } |
---|
96 | _field_file_cache($file); // cache the file in order to minimize load queries |
---|
97 | return (array)$file; |
---|
98 | } |
---|
99 | |
---|
100 | /** |
---|
101 | * Save a file into a file node after running all the associated validators. |
---|
102 | * |
---|
103 | * This function is usually used to move a file from the temporary file |
---|
104 | * directory to a permanent location. It may be used by import scripts or other |
---|
105 | * modules that want to save an existing file into the database. |
---|
106 | * |
---|
107 | * @param $filepath |
---|
108 | * The local file path of the file to be saved. |
---|
109 | * @param $validators |
---|
110 | * An optional, associative array of callback functions used to validate the |
---|
111 | * file. The keys are function names and the values arrays of callback |
---|
112 | * parameters which will be passed in after the user and file objects. The |
---|
113 | * functions should return an array of error messages, an empty array |
---|
114 | * indicates that the file passed validation. The functions will be called in |
---|
115 | * the order specified. |
---|
116 | * @param $dest |
---|
117 | * A string containing the directory $source should be copied to. If this is |
---|
118 | * not provided or is not writable, the temporary directory will be used. |
---|
119 | * @param $account |
---|
120 | * The user account object that should associated with the uploaded file. |
---|
121 | * @return |
---|
122 | * An array containing the file information, or 0 in the event of an error. |
---|
123 | */ |
---|
124 | function field_file_save_file($filepath, $validators = array(), $dest = FALSE, $account = NULL) { |
---|
125 | if (!isset($account)) { |
---|
126 | $account = $GLOBALS['user']; |
---|
127 | } |
---|
128 | |
---|
129 | // Add in our check of the the file name length. |
---|
130 | $validators['file_validate_name_length'] = array(); |
---|
131 | |
---|
132 | // Begin building file object. |
---|
133 | $file = new stdClass(); |
---|
134 | $file->uid = $account->uid; |
---|
135 | $file->filename = basename($filepath); |
---|
136 | $file->filepath = $filepath; |
---|
137 | $file->filemime = module_exists('mimedetect') ? mimedetect_mime($file) : file_get_mimetype($file->filename); |
---|
138 | |
---|
139 | // Rename potentially executable files, to help prevent exploits. |
---|
140 | if (preg_match('/\.(php|pl|py|cgi|asp|js)$/i', $file->filename) && (substr($file->filename, -4) != '.txt')) { |
---|
141 | $file->filemime = 'text/plain'; |
---|
142 | $file->filepath .= '.txt'; |
---|
143 | $file->filename .= '.txt'; |
---|
144 | } |
---|
145 | |
---|
146 | // If the destination is not provided, or is not writable, then use the |
---|
147 | // temporary directory. |
---|
148 | if (empty($dest) || file_check_path($dest) === FALSE) { |
---|
149 | $dest = file_directory_temp(); |
---|
150 | } |
---|
151 | |
---|
152 | $file->source = 'field_file_save_file'; |
---|
153 | $file->destination = file_destination(file_create_path($dest .'/'. $file->filename), FILE_EXISTS_RENAME); |
---|
154 | $file->filesize = filesize($filepath); |
---|
155 | |
---|
156 | // Call the validation functions. |
---|
157 | $errors = array(); |
---|
158 | foreach ($validators as $function => $args) { |
---|
159 | // Add the $file variable to the list of arguments and pass it by |
---|
160 | // reference (required for PHP 5.3 and higher). |
---|
161 | array_unshift($args, NULL); |
---|
162 | $args[0] = &$file; |
---|
163 | $errors = array_merge($errors, call_user_func_array($function, $args)); |
---|
164 | } |
---|
165 | |
---|
166 | // Check for validation errors. |
---|
167 | if (!empty($errors)) { |
---|
168 | $message = t('The selected file %name could not be saved.', array('%name' => $file->filename)); |
---|
169 | if (count($errors) > 1) { |
---|
170 | $message .= '<ul><li>'. implode('</li><li>', $errors) .'</li></ul>'; |
---|
171 | } |
---|
172 | else { |
---|
173 | $message .= ' '. array_pop($errors); |
---|
174 | } |
---|
175 | form_set_error($file->source, $message); |
---|
176 | return 0; |
---|
177 | } |
---|
178 | |
---|
179 | if (!file_copy($file, $file->destination, FILE_EXISTS_RENAME)) { |
---|
180 | form_set_error($file->source, t('File upload error. Could not move uploaded file.')); |
---|
181 | watchdog('file', 'Upload error. Could not move file %file to destination %destination.', array('%file' => $file->filename, '%destination' => $file->destination)); |
---|
182 | return 0; |
---|
183 | } |
---|
184 | |
---|
185 | // If we made it this far it's safe to record this file in the database. |
---|
186 | $file->status = FILE_STATUS_TEMPORARY; |
---|
187 | $file->timestamp = time(); |
---|
188 | // Insert new record to the database. |
---|
189 | drupal_write_record('files', $file); |
---|
190 | |
---|
191 | // Let modules add additional properties to the yet barebone file object. |
---|
192 | foreach (module_implements('file_insert') as $module) { |
---|
193 | $function = $module .'_file_insert'; |
---|
194 | $function($file); |
---|
195 | } |
---|
196 | _field_file_cache($file); // cache the file in order to minimize load queries |
---|
197 | return (array)$file; |
---|
198 | } |
---|
199 | |
---|
200 | /** |
---|
201 | * Save a node file. Delete items if necessary and set new items as permanent. |
---|
202 | * |
---|
203 | * @param $node |
---|
204 | * Node object this file is be associated with. |
---|
205 | * @param $file |
---|
206 | * File to be inserted, passed by reference since fid should be attached. |
---|
207 | * @return array |
---|
208 | */ |
---|
209 | function field_file_save($node, &$file) { |
---|
210 | // If this item is marked for deletion. |
---|
211 | if (!empty($file['delete']) || !empty($file['_remove'])) { |
---|
212 | // If we're creating a new revision, return an empty array so CCK will |
---|
213 | // remove the item. |
---|
214 | if (!empty($node->old_vid)) { |
---|
215 | return array(); |
---|
216 | } |
---|
217 | // Otherwise delete the file and return an empty array. |
---|
218 | if (field_file_delete($file)) { |
---|
219 | return array(); |
---|
220 | } |
---|
221 | } |
---|
222 | |
---|
223 | // Cast to object since core functions use objects. |
---|
224 | $file = (object)$file; |
---|
225 | |
---|
226 | // Set permanent status on files if unset. |
---|
227 | if (empty($file->status)) { |
---|
228 | file_set_status($file, FILE_STATUS_PERMANENT); |
---|
229 | } |
---|
230 | |
---|
231 | // Let modules update their additional file properties too. |
---|
232 | foreach (module_implements('file_update') as $module) { |
---|
233 | $function = $module .'_file_update'; |
---|
234 | $function($file); |
---|
235 | } |
---|
236 | _field_file_cache($file); // update the cache, in case the file has changed |
---|
237 | |
---|
238 | $file = (array)$file; |
---|
239 | return $file; |
---|
240 | } |
---|
241 | |
---|
242 | /** |
---|
243 | * Delete a field file and its database record. |
---|
244 | * |
---|
245 | * @param $path |
---|
246 | * A file object. |
---|
247 | * @param $force |
---|
248 | * Force File Deletion ignoring reference counting. |
---|
249 | * @return mixed |
---|
250 | * TRUE for success, Array for reference count block, or FALSE in the event of an error. |
---|
251 | */ |
---|
252 | function field_file_delete($file, $force = FALSE) { |
---|
253 | $file = (object)$file; |
---|
254 | // If any module returns a value from the reference hook, the |
---|
255 | // file will not be deleted from Drupal, but file_delete will |
---|
256 | // return a populated array that tests as TRUE. |
---|
257 | if (!$force && $references = module_invoke_all('file_references', $file)) { |
---|
258 | $references = array_filter($references); // only keep positive values |
---|
259 | if (!empty($references)) { |
---|
260 | return $references; |
---|
261 | } |
---|
262 | } |
---|
263 | |
---|
264 | // Let other modules clean up on delete. |
---|
265 | module_invoke_all('file_delete', $file); |
---|
266 | |
---|
267 | // Make sure the file is deleted before removing its row from the |
---|
268 | // database, so UIs can still find the file in the database. |
---|
269 | if (file_delete($file->filepath)) { |
---|
270 | db_query('DELETE FROM {files} WHERE fid = %d', $file->fid); |
---|
271 | _field_file_cache(NULL, $file); // delete the file from the cache |
---|
272 | return TRUE; |
---|
273 | } |
---|
274 | return FALSE; |
---|
275 | } |
---|
276 | |
---|
277 | /** |
---|
278 | * Internal cache, in order to minimize database queries for loading files. |
---|
279 | */ |
---|
280 | function _field_file_cache($file = NULL, $reset = FALSE) { |
---|
281 | static $files = array(); |
---|
282 | |
---|
283 | // Reset internal cache. |
---|
284 | if (is_object($reset)) { // file object, uncache just that one |
---|
285 | unset($files[$reset->fid]); |
---|
286 | unset($files[$reset->filepath]); |
---|
287 | } |
---|
288 | else if ($reset) { // TRUE, delete the whole cache |
---|
289 | $files = array(); |
---|
290 | } |
---|
291 | |
---|
292 | // Cache the file by both fid and filepath. |
---|
293 | // Use non-copying objects to save memory. |
---|
294 | if (!empty($file->fid)) { |
---|
295 | $files[$file->fid] = $file; |
---|
296 | $files[$file->filepath] = $file; |
---|
297 | } |
---|
298 | return $files; |
---|
299 | } |
---|
300 | |
---|
301 | /** |
---|
302 | * A silent version of file.inc's file_check_directory(). |
---|
303 | * |
---|
304 | * This function differs from file_check_directory in that it checks for |
---|
305 | * files when doing the directory check and it does not use drupal_set_message() |
---|
306 | * when creating directories. This function may be removed in Drupal 7. |
---|
307 | * |
---|
308 | * Check that the directory exists and is writable. Directories need to |
---|
309 | * have execute permissions to be considered a directory by FTP servers, etc. |
---|
310 | * |
---|
311 | * @param $directory A string containing the name of a directory path. |
---|
312 | * @param $mode A Boolean value to indicate if the directory should be created |
---|
313 | * if it does not exist or made writable if it is read-only. |
---|
314 | * @param $form_item An optional string containing the name of a form item that |
---|
315 | * any errors will be attached to. This is useful for settings forms that |
---|
316 | * require the user to specify a writable directory. If it can't be made to |
---|
317 | * work, a form error will be set preventing them from saving the settings. |
---|
318 | * @return FALSE when directory not found, or TRUE when directory exists. |
---|
319 | */ |
---|
320 | function field_file_check_directory(&$directory, $mode = 0, $form_item = NULL) { |
---|
321 | $directory = rtrim($directory, '/\\'); |
---|
322 | |
---|
323 | // Error if the directory is a file. |
---|
324 | if (is_file($directory)) { |
---|
325 | watchdog('file system', 'The path %directory was checked as a directory, but it is a file.', array('%directory' => $directory), WATCHDOG_ERROR); |
---|
326 | if ($form_item) { |
---|
327 | form_set_error($form_item, t('The directory %directory is a file and cannot be overwritten.', array('%directory' => $directory))); |
---|
328 | } |
---|
329 | return FALSE; |
---|
330 | } |
---|
331 | |
---|
332 | // Create the directory if it is missing. |
---|
333 | if (!is_dir($directory) && $mode & FILE_CREATE_DIRECTORY && !@mkdir($directory, 0775, TRUE)) { |
---|
334 | watchdog('file system', 'The directory %directory does not exist.', array('%directory' => $directory), WATCHDOG_ERROR); |
---|
335 | if ($form_item) { |
---|
336 | form_set_error($form_item, t('The directory %directory does not exist.', array('%directory' => $directory))); |
---|
337 | } |
---|
338 | return FALSE; |
---|
339 | } |
---|
340 | |
---|
341 | // Check to see if the directory is writable. |
---|
342 | if (!is_writable($directory) && $mode & FILE_MODIFY_PERMISSIONS && !@chmod($directory, 0775)) { |
---|
343 | watchdog('file system', 'The directory %directory is not writable, because it does not have the correct permissions set.', array('%directory' => $directory), WATCHDOG_ERROR); |
---|
344 | if ($form_item) { |
---|
345 | form_set_error($form_item, t('The directory %directory is not writable', array('%directory' => $directory))); |
---|
346 | } |
---|
347 | return FALSE; |
---|
348 | } |
---|
349 | |
---|
350 | if ((file_directory_path() == $directory || file_directory_temp() == $directory) && !is_file("$directory/.htaccess")) { |
---|
351 | $htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nOptions None\nOptions +FollowSymLinks"; |
---|
352 | if (($fp = fopen("$directory/.htaccess", 'w')) && fputs($fp, $htaccess_lines)) { |
---|
353 | fclose($fp); |
---|
354 | chmod($directory .'/.htaccess', 0664); |
---|
355 | } |
---|
356 | else { |
---|
357 | $repl = array('%directory' => $directory, '!htaccess' => nl2br(check_plain($htaccess_lines))); |
---|
358 | form_set_error($form_item, t("Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines:<br /><code>!htaccess</code>", $repl)); |
---|
359 | watchdog('security', "Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines:<br /><code>!htaccess</code>", $repl, WATCHDOG_ERROR); |
---|
360 | } |
---|
361 | } |
---|
362 | |
---|
363 | return TRUE; |
---|
364 | } |
---|
365 | |
---|
366 | /** |
---|
367 | * Remove a possible leading file directory path from the given path. |
---|
368 | */ |
---|
369 | function field_file_strip_path($path) { |
---|
370 | $dirpath = file_directory_path(); |
---|
371 | $dirlen = drupal_strlen($dirpath); |
---|
372 | if (drupal_substr($path, 0, $dirlen + 1) == $dirpath .'/') { |
---|
373 | $path = drupal_substr($path, $dirlen + 1); |
---|
374 | } |
---|
375 | return $path; |
---|
376 | } |
---|
377 | |
---|
378 | /** |
---|
379 | * Encode a file path in a way that is compatible with file_create_url(). |
---|
380 | * |
---|
381 | * This function should be used on the $file->filepath property before any call |
---|
382 | * to file_create_url(). This ensures that the file directory path prefix is |
---|
383 | * unmodified, but the actual path to the file will be properly URL encoded. |
---|
384 | */ |
---|
385 | function field_file_urlencode_path($path) { |
---|
386 | // Encode the parts of the path to ensure URLs operate within href attributes. |
---|
387 | // Private file paths are urlencoded for us inside of file_create_url(). |
---|
388 | if (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PUBLIC) { |
---|
389 | $file_directory_path = file_directory_path(); |
---|
390 | if (strpos($path, $file_directory_path . '/') === 0) { |
---|
391 | $path = trim(substr($path, strlen($file_directory_path)), '\\/'); |
---|
392 | } |
---|
393 | |
---|
394 | $parts = explode('/', $path); |
---|
395 | foreach ($parts as $index => $part) { |
---|
396 | $parts[$index] = rawurlencode($part); |
---|
397 | } |
---|
398 | $path = implode('/', $parts); |
---|
399 | |
---|
400 | // Add the file directory path again (not encoded). |
---|
401 | $path = $file_directory_path . '/' . $path; |
---|
402 | } |
---|
403 | return $path; |
---|
404 | } |
---|
405 | |
---|
406 | /** |
---|
407 | * Return a count of the references to a file by all modules. |
---|
408 | */ |
---|
409 | function field_file_references($file) { |
---|
410 | $references = (array) module_invoke_all('file_references', $file); |
---|
411 | $reference_count = 0; |
---|
412 | foreach ($references as $module => $count) { |
---|
413 | $reference_count += $count; |
---|
414 | } |
---|
415 | return $reference_count; |
---|
416 | } |
---|