=== modified file 'includes/file.inc' --- includes/file.inc 2008-12-04 11:09:33 +0000 +++ includes/file.inc 2008-12-29 04:57:20 +0000 @@ -84,41 +84,106 @@ define('FILE_STATUS_TEMPORARY', 0); */ define('FILE_STATUS_PERMANENT', 1); + +/** + * Create download path to a file. + * + * Unlike _file_create_url this takes a file object as input rather than a path. + * + * @param &$file + * A file object. + * @return + * A string containing the path to the desired file + */ +function file_create_url(&$file) { + // Avoid duplication with _file_create_url + if (!isset($file->private)) { + $file->private = FILE_DOWNLOADS_PUBLIC; + } + + return _file_create_url($file->filename, $file->private); +} + + /** * Create the download path to a file. * * @param $path A string containing the path of the file to generate URL for. + * @param $private An integer indicated whether the file is public (FILE_DOWNLOADS_PUBLIC) or private (FILE_DOWNLOADS_PRIVATE). * @return A string containing a URL that can be used to download the file. */ -function file_create_url($path) { - // Strip file_directory_path from $path. We only include relative paths in - // URLs. +function _file_create_url($path, $private = FILE_DOWNLOADS_PUBLIC) { + // Strip file_directory_path from $path. We only include relative paths in urls. if (strpos($path, file_directory_path() . '/') === 0) { $path = trim(substr($path, strlen(file_directory_path())), '\\/'); } - switch (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC)) { - case FILE_DOWNLOADS_PUBLIC: - return $GLOBALS['base_url'] . '/' . file_directory_path() . '/' . str_replace('\\', '/', $path); - case FILE_DOWNLOADS_PRIVATE: - return url('system/files/' . $path, array('absolute' => TRUE)); + + // Check if the file is private + if ($private == FILE_DOWNLOADS_PRIVATE) { + // If it is private return a private file path + return url('system/files/'.$path, array('absolute' => TRUE)); + else { + // Otherwise return a public file path + return $GLOBALS['base_url'] . '/' . file_directory_path() . '/' . str_replace('\\', '/', $path); } } + /** * Make sure the destination is a complete path and resides in the file system * directory, if it is not prepend the file system directory. * - * @param $destination - * A string containing the path to verify. If this value is omitted, Drupal's - * 'files' directory will be used. - * @return - * A string containing the path to file, with file system directory appended - * if necessary, or FALSE if the path is invalid (i.e. outside the configured - * 'files' or temp directories). - */ -function file_create_path($destination = NULL) { - $file_path = file_directory_path(); - if (is_null($destination)) { + * Unlike _file_create_path this function takes a file object as input rather than a path. + * + * @param $file + * A file object containing the path to verify and the public/private + * status of the file. + * @return + * A string containing the path to file, with file system directory + * appended if necessary, or FALSE if the path is invalid (i.e. outside the + * configured 'files' or temp directories). + */ +function file_create_path(&$file) { + + if (!isset($file->private)) { + $file->private = FILE_DOWNLOADS_PUBLIC; + } + + // Avoiding Duplication with _file_create_path + return _file_create_path($file->filename, $file->private); +} + +/** + * Make sure the destination is a complete path and resides in the file system + * directory, if it is not prepend the file system directory. + * + * @param $dest A string containing the path to verify. If this value is + * omitted, Drupal's 'files' directory will be used. + * @param $private An integer containg FILE_DOWNLOADS_PUBLIC if the public file path + * should be checked or a FILE_DOWNLOADS_PRIVATE if the private file path should be checked. + * This should be omitted if it isn't known whether a file will reside + * in the public or private file path. + * @return A string containing the path to file, with file system directory + * appended if necessary, or FALSE if the path is invalid (i.e. outside the + * configured 'files' or temp directories). + */ +function _file_create_path($destination = 0, $private = NULL) { + + /* + A small performance hit will be necessary in some cases because it won't + be clear from the calling function whether a file will be residing in + the public or private file path. So in that case we query the database + to determine if the file is public or private. + */ + if ($private == NULL) { + $private = db_query("SELECT private FROM {files} WHERE filepath = :path", array(':path' => $destination))->fetchField(); + } + + // If private look in private files directory otherwise look in public files directory + $file_path = ($private == FILE_DOWNLOADS_PRIVATE) ? file_directory_private_path() : file_directory_path(); + + // Return file_directory_path if $dest is not supplied + if (!$destination) { return $file_path; } // file_check_location() checks whether the destination is inside the Drupal @@ -140,24 +205,17 @@ function file_create_path($destination = } /** - * Check that the directory exists and is writable. - * - * Directories need to have execute permissions to be considered a directory by - * FTP servers, etc. + * Check that the directory exists and is writable. Directories need to + * have execute permissions to be considered a directory by FTP servers, etc. * - * @param $directory - * A string containing the name of a directory path. - * @param $mode - * A bitmask to indicate if the directory should be created if it does - * not exist (FILE_CREATE_DIRECTORY) or made writable if it is read-only - * (FILE_MODIFY_PERMISSIONS). - * @param $form_item - * An optional string containing the name of a form item that any errors will - * be attached to. This is useful for settings forms that require the user to - * specify a writable directory. If it can't be made to work, a form error - * will be set preventing them from saving the settings. - * @return - * FALSE when directory not found, or TRUE when directory exists. + * @param $directory A string containing the name of a directory path. + * @param $mode A Boolean value to indicate if the directory should be created + * if it does not exist or made writable if it is read-only. + * @param $form_item An optional string containing the name of a form item that + * any errors will be attached to. This is useful for settings forms that + * require the user to specify a writable directory. If it can't be made to + * work, a form error will be set preventing them from saving the settings. + * @return FALSE when directory not found, or TRUE when directory exists. */ function file_check_directory(&$directory, $mode = 0, $form_item = NULL) { $directory = rtrim($directory, '/\\'); @@ -178,13 +236,9 @@ function file_check_directory(&$director // Check to see if the directory is writable. if (!is_writable($directory)) { - // If not able to modify permissions, or if able to, but chmod - // fails, return false. - if (!$mode || (($mode & FILE_MODIFY_PERMISSIONS) && !@chmod($directory, 0775))) { - if ($form_item) { - form_set_error($form_item, t('The directory %directory is not writable', array('%directory' => $directory))); - watchdog('file system', 'The directory %directory is not writable, because it does not have the correct permissions set.', array('%directory' => $directory), WATCHDOG_ERROR); - } + if (($mode & FILE_MODIFY_PERMISSIONS) && !@chmod($directory, 0775)) { + form_set_error($form_item, t('The directory %directory is not writable', array('%directory' => $directory))); + watchdog('file system', 'The directory %directory is not writable, because it does not have the correct permissions set.', array('%directory' => $directory), WATCHDOG_ERROR); return FALSE; } } @@ -236,7 +290,7 @@ function file_check_path(&$path) { * This should be used to make sure a file specified is really located within * the directory to prevent exploits. Note that the file or path being checked * does not actually need to exist yet. - * + * * @code * // Returns FALSE: * file_check_location('/www/example.com/files/../../../etc/passwd', '/www/example.com/files'); @@ -435,7 +489,7 @@ function file_unmanaged_copy($source, $d return FALSE; } - $destination = file_create_path($destination); + $destination = file_create_path($destination, FILE_DOWNLOADS_PUBLIC); $directory = $destination; $basename = file_check_path($directory); @@ -869,7 +923,7 @@ function file_save_upload($source, $vali } $file->source = $source; - $file->destination = file_destination(file_create_path($destination . '/' . $file->filename), $replace); + $file->destination = file_destination(_file_create_path($destination . '/' . $file->filename, FILE_DOWNLOADS_PUBLIC), $replace); // Call the validation functions specified by this function's caller. $errors = file_validate($file, $validators); @@ -1197,7 +1251,7 @@ function file_transfer($source, $headers drupal_set_header($header); } - $source = file_create_path($source); + $source = file_create_path($source, FILE_DOWNLOADS_PRIVATE); // Transfer file in 1024 byte chunks to save memory usage. if ($fd = fopen($source, 'rb')) { @@ -1233,16 +1287,29 @@ function file_download() { $filepath = $_GET['file']; } - if (file_exists(file_create_path($filepath))) { + if (file_exists(file_create_path($filepath, FILE_DOWNLOADS_PRIVATE))) { // Let other modules provide headers and controls access to the file. $headers = module_invoke_all('file_download', $filepath); if (in_array(-1, $headers)) { return drupal_access_denied(); } if (count($headers)) { - file_transfer($filepath, $headers); + + // See if the user already has a key for the file, if they do we just transfer them to that + $key = db_query("SELECT c.file_key FROM {file_checkout} c INNER JOIN {files} f ON c.fid=f.fid WHERE f.uid = :uid AND f.filepath = :filepath", array(':uid' => $user->uid, ':filepath' => $filepath))->fetchField();; + + + if (!$key) { + // Otherwise we create a key, and send them to that + $key = md5(drupal_random_bytes(8)); + $fid = db_query("SELECT fid FROM {files} WHERE filename = :path",array(':path' => $filepath))->fetchField(); + + db_query("INSERT INTO {file_checkout} (file_key, fid, uid, checkout_timestamp, headers) VALUES (:key, :fid, :uid, :timestamp, :headers)", array(':key' => $key, ':fid' => $fid, ':uid' => $user->uid, ':timestamp' => time(), ':headers' => serialize($headers))); + } + drupal_goto('private.php', "key=$key"); } } + return drupal_not_found(); } @@ -1365,6 +1432,18 @@ function file_directory_path() { } /** + * Determine the default private files directory. + * + * The default directory is left blank to prevent it from being inside + * the webserver's root directory. + * + * @return A string containing the path to Drupal's 'private-files' directory. + */ +function file_directory_private_path() { + return variable_get('file_private_path', ''); +} + +/** * Determine the maximum file upload size by querying the PHP settings. * * @return @@ -1383,6 +1462,60 @@ function file_upload_max_size() { } /** +* Set file accessibility in Drupal's database and update file path accordingly. +* +* @param +* A file object that has had its "private" variable updated to +* indicate whether the file should be public or private. +* - To have the file be privately accessible the "private" field should be set to FILE_DOWNLOADS_PRIVATE. +* - To have the file be publicly accessible the "private" field should be set to FILE_DOWNLOADS_PUBLC. +* @return +* A boolean with TRUE if the operation was successful and FALSE if the operation failed. +*/ +function file_set_private(&$file) { + + // Make sure private member is set + if (!isset($file->private)) { + $file->private = FILE_DOWNLOADS_PUBLIC; + } + + // If file is private we check the private directory to make sure it is writable, if not the directory is created + if ($file->private == FILE_DOWNLOADS_PRIVATE && file_check_directory(file_directory_private_path(), FILE_CREATE_DIRECTORY)) { + + // Move file to private path + if (file_move($file, file_directory_private_path(), FILE_EXISTS_RENAME, FILE_DOWNLOADS_PRIVATE)) { + $file->filename = basename($file->filepath); + db_query("UPDATE {files} SET private = :priv, filepath = :path, filename = :name WHERE fid = :fid", + array(":priv" => $file->private, ":path" => $file->filepath, ":name" => $file->filename, ":fid" =>$file->fid)); + return TRUE; + } + elseif ($file->private) { + drupal_set_message (t('The chosen private path is not writable or cannot be created, please choose another.'), 'error'); + return FALSE; + } + else { + drupal_set_message (t('The chosen file could not be moved to the private path.'), 'error'); + return FALSE; + } + } + + // If this point has be reached the file is public + if (file_check_directory(file_directory_path(), FILE_CREATE_DIRECTORY)) { + if (file_move($file->filepath, file_directory_path(), FILE_EXISTS_RENAME, FILE_DOWNLOADS_PUBLIC)) { + $file->filename = basename($file->filepath); + db_query("UPDATE {files} SET private = :priv, filepath = :path, filename = :name WHERE fid = :fid", + array(":priv" => $file->private, ":path" => $file->filepath, ":name" => $file->filename, ":fid" =>$file->fid)); + return TRUE; + } + else { + drupal_set_message(t('The chosen file could not be moved to the public path.'), 'error'); + } + } + + return FALSE; + } + +/** * Determine an Internet Media Type, or MIME type from a filename. * * @param $filename === modified file 'includes/locale.inc' --- includes/locale.inc 2008-12-19 03:55:23 +0000 +++ includes/locale.inc 2008-12-29 03:49:34 +0000 @@ -2191,11 +2191,11 @@ function _locale_rebuild_js($langcode = // Construct the filepath where JS translation files are stored. // There is (on purpose) no front end to edit that variable. - $dir = file_create_path(variable_get('locale_js_directory', 'languages')); + $dir = _file_create_path(variable_get('locale_js_directory', 'languages'), FILE_DOWNLOADS_PUBLIC); // Delete old file, if we have no translations anymore, or a different file to be saved. if (!empty($language->javascript) && (!$data || $language->javascript != $data_hash)) { - file_delete(file_create_path($dir . '/' . $language->language . '_' . $language->javascript . '.js')); + file_delete(_file_create_path($dir . '/' . $language->language . '_' . $language->javascript . '.js', FILE_DOWNLOADS_PUBLIC)); $language->javascript = ''; $status = 'deleted'; } === modified file 'modules/blogapi/blogapi.module' --- modules/blogapi/blogapi.module 2008-12-08 21:54:31 +0000 +++ modules/blogapi/blogapi.module 2008-12-29 03:49:36 +0000 @@ -478,7 +478,7 @@ function blogapi_metaweblog_new_media_ob drupal_write_record('blogapi_files', $row); // Return the successful result. - return array('url' => file_create_url($file), 'struct'); + return array('url' => _file_create_url($file, FILE_DOWNLOADS_PUBLIC), 'struct'); } /** * Blogging API callback. Returns a list of the taxonomy terms that can be === modified file 'modules/locale/locale.install' --- modules/locale/locale.install 2008-12-20 18:24:32 +0000 +++ modules/locale/locale.install 2008-12-29 03:49:38 +0000 @@ -213,7 +213,7 @@ function locale_uninstall() { $files = db_query('SELECT javascript FROM {languages}'); while ($file = db_fetch_object($files)) { if (!empty($file)) { - file_delete(file_create_path($file->javascript)); + file_delete(file_create_path($file)); } } === modified file 'modules/system/system.admin.inc' --- modules/system/system.admin.inc 2008-11-26 13:54:05 +0000 +++ modules/system/system.admin.inc 2008-12-29 03:49:40 +0000 @@ -1446,6 +1446,15 @@ function system_file_system_settings() { '#after_build' => array('system_check_directory'), ); + $form['file_private_path'] = array( + '#type' => 'textfield', + '#title' => t('Private file path'), + '#default_value' => file_directory_private_path(), + '#maxlength' => 255, + '#description' => t('A file system path where priavte files will be stored. This directory has to exist and be writable by Drupal. Changing this location after the site has been in use will cause problems so only change this setting on an existing site if you know what you are doing.'), + '#after_build' => array('system_check_directory','system_check_private_path'), + ); + $form['file_directory_temp'] = array( '#type' => 'textfield', '#title' => t('Temporary directory'), @@ -1455,14 +1464,6 @@ function system_file_system_settings() { '#after_build' => array('system_check_directory'), ); - $form['file_downloads'] = array( - '#type' => 'radios', - '#title' => t('Download method'), - '#default_value' => variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC), - '#options' => array(FILE_DOWNLOADS_PUBLIC => t('Public - files are available using HTTP directly.'), FILE_DOWNLOADS_PRIVATE => t('Private - files are transferred by Drupal.')), - '#description' => t('Choose the Public download method unless you wish to enforce fine-grained access controls over file downloads. Changing the download method will modify all download paths and may cause unexpected problems on an existing site.') - ); - return system_settings_form($form); } === modified file 'modules/system/system.module' --- modules/system/system.module 2008-12-28 19:11:31 +0000 +++ modules/system/system.module 2008-12-29 03:49:40 +0000 @@ -1000,7 +1000,7 @@ function system_theme_select_form($descr /** * Checks the existence of the directory specified in $form_element. This * function is called from the system_settings form to check both the - * file_directory_path and file_directory_temp directories. If validation + * file_directory_path, file_private_path and file_directory_temp directories. If validation * fails, the form element is flagged with an error from within the * file_check_directory function. * @@ -1012,6 +1012,32 @@ function system_check_directory($form_el return $form_element; } +/** + * See if the chosen private files path in the web root. + * + * Because private files would be accessible if they were placed in the webserver's root + * we check to make sure that the private files directory is not there. + * + * @param $form_element + * The form element containing the name of the directory to prevent access to. + */ + function system_check_private_path($form_element) { + $location = $form_element['#value']; + $web_root = $_SERVER['DOCUMENT_ROOT']; + $directory_bad = FALSE; + + // Make sure location is an absolute path + $location = realpath($location); + + // See if the webserver's root directory begins the path + if (0 === strpos($location, $web_root)) { + form_set_error($form_element['#parents'][0], 'The private files directory specified is inside of the webserver\'s root directory. + Please choose a directory which lies outside of the webserver\'s root directory.'); + } + + return $form_element; +} + /** * Retrieves the current status of an array of files in the system table. * === modified file 'modules/upload/upload.admin.inc' --- modules/upload/upload.admin.inc 2008-07-16 21:59:24 +0000 +++ modules/upload/upload.admin.inc 2008-10-25 19:49:02 +0000 @@ -80,6 +80,14 @@ function upload_admin_settings() { '#options' => array(0 => t('No'), 1 => t('Yes')), '#description' => t('Display attached files when viewing a post.'), ); + + $form['settings_general']['upload_private_default'] = array( + '#type' => 'radios', + '#title' => t('Upload access default'), + '#default_value' => variable_get('upload_private_default', FILE_DOWNLOADS_PUBLIC), + '#options' => array(FILE_DOWNLOADS_PUBLIC => t('Public - files are available using HTTP directly.'), FILE_DOWNLOADS_PRIVATE => t('Private - files are transferred by Drupal.')), + '#description' => t('This determines whether file uploads will be publicly or privately accessible by default'), + ); $form['settings_general']['upload_extensions_default'] = array( '#type' => 'textfield', === modified file 'modules/upload/upload.module' --- modules/upload/upload.module 2008-12-16 22:05:50 +0000 +++ modules/upload/upload.module 2008-12-29 05:02:13 +0000 @@ -151,8 +151,8 @@ function _upload_file_limits($user) { * Implementation of hook_file_download(). */ function upload_file_download($filepath) { - $filepath = file_create_path($filepath); - $result = db_query("SELECT f.*, u.nid FROM {files} f INNER JOIN {upload} u ON f.fid = u.fid WHERE filepath = '%s'", $filepath); + $filepath = file_create_path($filepath, FILE_DOWNLOADS_PRIVATE); + $result = db_query("SELECT f.*, u.nid FROM {files} f INNER JOIN {upload} u ON f.fid = u.fid WHERE filepath = :filepath", array(':filepath' => $filepath)); if ($file = db_fetch_object($result)) { if (user_access('view uploaded files') && ($node = node_load($file->nid)) && node_access('view', $node)) { return array( @@ -479,10 +479,47 @@ function upload_save(&$node) { // Create a new revision, or associate a new file needed. if (!empty($node->old_vid) || $file->new) { db_query("INSERT INTO {upload} (fid, nid, vid, list, description, weight) VALUES (%d, %d, %d, %d, '%s', %d)", $file->fid, $node->nid, $node->vid, $file->list, $file->description, $file->weight); + file_set_private($file); } // Update existing revision. else { db_query("UPDATE {upload} SET list = %d, description = '%s', weight = %d WHERE fid = %d AND vid = %d", $file->list, $file->description, $file->weight, $file->fid, $node->vid); + file_set_private($file); + } + } + // Empty the session storage after save. We use this variable to track files + // that haven't been related to the node yet. + unset($_SESSION['upload_files']); +} + +function upload_delete($node) { + $files = array(); + $result = db_query('SELECT DISTINCT f.* FROM {upload} u INNER JOIN {files} f ON u.fid = f.fid WHERE u.nid = %d', $node->nid); + while ($file = db_fetch_object($result)) { + $files[$file->fid] = $file; + } + + foreach ($files as $fid => $file) { + // Delete all files associated with the node + db_query('DELETE FROM {files} WHERE fid = %d', $fid); + file_delete($file->filepath); + } + + // Delete all file revision information associated with the node + db_query('DELETE FROM {upload} WHERE nid = %d', $node->nid); +} + +function upload_delete_revision($node) { + if (is_array($node->files)) { + foreach ($node->files as $file) { + // Check if the file will be used after this revision is deleted + $count = db_result(db_query('SELECT COUNT(fid) FROM {upload} WHERE fid = %d', $file->fid)); + + // if the file won't be used, delete it + if ($count < 2) { + db_query('DELETE FROM {files} WHERE fid = %d', $file->fid); + file_delete($file->filepath); + } } $file->status &= FILE_STATUS_PERMANENT; $file = file_save($file); @@ -501,11 +538,11 @@ function _upload_form($node) { $form['files']['#theme'] = 'upload_form_current'; $form['files']['#tree'] = TRUE; foreach ($node->files as $file) { + $key = $file['key']; $file = (object)$file; - $key = $file->fid; - $form['files'][$key]['description'] = array('#type' => 'textfield', '#default_value' => !empty($file->description) ? $file->description : $file->filename, '#maxlength' => 256, '#description' => '' . file_create_url($file->filepath) . ''); - $form['files'][$key]['size'] = array('#markup' => format_size($file->filesize)); + $form['files'][$key]['size'] = array('#value' => format_size($file->filesize)); + $form['files'][$key]['private'] = array('#type' => 'radios', '#options' => array(FILE_DOWNLOADS_PUBLIC => 'Public', FILE_DOWNLOADS_PRIVATE => 'Private'), '#default_value' => $file->private ); $form['files'][$key]['remove'] = array('#type' => 'checkbox', '#default_value' => !empty($file->remove)); $form['files'][$key]['list'] = array('#type' => 'checkbox', '#default_value' => $file->list); $form['files'][$key]['weight'] = array('#type' => 'weight', '#delta' => count($node->files), '#default_value' => $file->weight); @@ -561,7 +598,7 @@ function _upload_form($node) { * @ingroup themeable */ function theme_upload_form_current(&$form) { - $header = array('', t('Delete'), t('List'), t('Description'), t('Weight'), t('Size')); + $header = array('', t('Delete'), t('List'), t('Accessibility'), t('Description'), t('Weight'), t('Size')); drupal_add_tabledrag('upload-attachments', 'order', 'sibling', 'upload-weight'); foreach (element_children($form) as $key) { @@ -571,6 +608,7 @@ function theme_upload_form_current(&$for $row = array(''); $row[] = drupal_render($form[$key]['remove']); $row[] = drupal_render($form[$key]['list']); + $row[] = drupal_render($form[$key]['private']); $row[] = drupal_render($form[$key]['description']); $row[] = drupal_render($form[$key]['weight']); $row[] = drupal_render($form[$key]['size']); === modified file 'modules/user/user.module' --- modules/user/user.module 2008-12-23 14:18:31 +0000 +++ modules/user/user.module 2008-12-29 03:49:42 +0000 @@ -597,7 +597,7 @@ function user_perm() { */ function user_file_download($file) { if (strpos($file, variable_get('user_picture_path', 'pictures') . '/picture-') === 0) { - $info = image_get_info(file_create_path($file)); + $info = image_get_info(_file_create_path($file, FILE_DOWNLOADS_PRIVATE)); return array('Content-type: ' . $info['mime_type']); } } @@ -881,6 +881,7 @@ function template_preprocess_user_pictur $account = $variables['account']; if (!empty($account->picture) && file_exists($account->picture)) { $picture = file_create_url($account->picture); + $picture = _file_create_url($account->picture, FILE_DOWNLOADS_PUBLIC); } elseif (variable_get('user_picture_default', '')) { $picture = variable_get('user_picture_default', ''); === added file 'private.php' --- private.php 1970-01-01 00:00:00 +0000 +++ private.php 2008-12-29 05:03:19 +0000 @@ -0,0 +1,55 @@ +checkout_time + $expiration_time * 60; + + +// If the expiration time has not been reached we can transfer the file + + +// Send HTTP headers +foreach (unserialize($file->headers) as $header) { + // To prevent HTTP header injection, we delete new lines that are + // not followed by a space or a tab. + // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 + $header = preg_replace('/\r?\n(?!\t| )/', '', $header); + header($header); +} + + +// Transfer file in 1024 byte chunks to save memory usage. +if ($fd = fopen($file->filepath, 'rb')) { + while (!feof($fd)) { + print fread($fd, 1024); + } + fclose($fd); +}