=== modified file 'includes/common.inc' --- includes/common.inc 2008-06-09 08:11:44 +0000 +++ includes/common.inc 2008-06-16 15:39:55 +0000 @@ -1760,7 +1760,7 @@ function drupal_build_css_cache($types, $data = ''; // Create the css/ within the files folder. - $csspath = file_create_path('css'); + $csspath = _file_create_path('css', FILE_DOWNLOADS_PUBLIC); file_check_directory($csspath, FILE_CREATE_DIRECTORY); if (!file_exists($csspath . '/' . $filename)) { @@ -1887,7 +1887,7 @@ function _drupal_load_stylesheet($matche * Delete all cached CSS files. */ function drupal_clear_css_cache() { - file_scan_directory(file_create_path('css'), '.*', array('.', '..', 'CVS'), 'file_delete', TRUE); + file_scan_directory(_file_create_path('css', FILE_DOWNLOADS_PUBLIC), '.*', array('.', '..', 'CVS'), 'file_delete', TRUE); } /** @@ -2232,7 +2232,7 @@ function drupal_build_js_cache($files, $ $contents = ''; // Create the js/ within the files folder. - $jspath = file_create_path('js'); + $jspath = _file_create_path('js', FILE_DOWNLOADS_PUBLIC); file_check_directory($jspath, FILE_CREATE_DIRECTORY); if (!file_exists($jspath . '/' . $filename)) { @@ -2255,7 +2255,7 @@ function drupal_build_js_cache($files, $ * Delete all cached JS files. */ function drupal_clear_js_cache() { - file_scan_directory(file_create_path('js'), '.*', array('.', '..', 'CVS'), 'file_delete', TRUE); + file_scan_directory(_file_create_path('js', FILE_DOWNLOADS_PUBLIC), '.*', array('.', '..', 'CVS'), 'file_delete', TRUE); variable_set('javascript_parsed', array()); } === modified file 'includes/file.inc' --- includes/file.inc 2008-05-26 17:12:54 +0000 +++ includes/file.inc 2008-06-18 04:47:07 +0000 @@ -70,40 +70,120 @@ 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) { +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)); // TODO: this will need to be changed when private.php is implemented + } + 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. + * + * 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) { + + + /* + * Note by Kyle C + * + * This should probably be updated to have better logic. Something along the lines + * of if private isn't set then we should query the database. + */ + 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($dest = 0) { - $file_path = file_directory_path(); +function _file_create_path($dest = 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) { + $result = db_query("SELECT private,status FROM {files} WHERE filepath='%s'", $dest); + while ($file = db_fetch_object($result)) { + $private = $file->private; + } + } + + // 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 (!$dest) { return $file_path; } + // file_check_location() checks whether the destination is inside the Drupal files directory. if (file_check_location($dest, $file_path)) { return $dest; @@ -249,11 +329,14 @@ function file_check_location($source, $d * - FILE_EXISTS_REPLACE - Replace the existing file * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is unique * - FILE_EXISTS_ERROR - Do nothing and return FALSE. - * @return True for success, FALSE for failure. + * @param $private An integer indicating whether the file should be copied to Drupal's + * public or private file path. + * FILE_DOWNLOADS_PUBLIC - The public file path should be used. + * FILE_DOWNLOADS_PRIVATE - The private file path should be used. + * @return TRUE for success, FALSE for failure. */ -function file_copy(&$source, $dest = 0, $replace = FILE_EXISTS_RENAME) { - $dest = file_create_path($dest); - +function file_copy(&$source, $dest = 0, $replace = FILE_EXISTS_RENAME, $private = FILE_DOWNLOADS_PUBLIC) { + $dest = _file_create_path($dest, $private); $directory = $dest; $basename = file_check_path($directory); @@ -363,12 +446,16 @@ function file_destination($destination, * - FILE_EXISTS_REPLACE - Replace the existing file * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is unique * - FILE_EXISTS_ERROR - Do nothing and return FALSE. + * @param $private An integer indicating whether the file should be moved to Drupal's + * public or private file path. + * - FILE_DOWNLOADS_PUBLIC - The public file path should be used. + * - FILE_DOWNLOADS_PRIAVTE - The private file path should be used. * @return True for success, FALSE for failure. */ -function file_move(&$source, $dest = 0, $replace = FILE_EXISTS_RENAME) { +function file_move(&$source, $dest = 0, $replace = FILE_EXISTS_RENAME, $private = FILE_DOWNLOADS_PUBLIC) { $path_original = is_object($source) ? $source->filepath : $source; - if (file_copy($source, $dest, $replace)) { + if (file_copy($source, $dest, $replace, $private)) { $path_current = is_object($source) ? $source->filepath : $source; if ($path_original == $path_current || file_delete($path_original)) { @@ -840,7 +927,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')) { @@ -872,7 +959,7 @@ function file_download() { $filepath = $_GET['file']; } - if (file_exists(file_create_path($filepath))) { + if (file_exists(_file_create_path($filepath, FILE_DOWNLOADS_PRIVATE))) { $headers = module_invoke_all('file_download', $filepath); if (in_array(-1, $headers)) { return drupal_access_denied(); @@ -1001,6 +1088,15 @@ function file_directory_path() { } /** + * Determine the default 'private-files' directory. + * + * @return A string containing the path to Drupal's 'private-files' directory. + */ +function file_directory_private_path() { + return variable_get('file_private_path', 'private-files'); +} + +/** * Determine the maximum file upload size by querying the PHP settings. * * @return @@ -1018,5 +1114,57 @@ 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_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); + $result = db_query("UPDATE {files} SET private='%d', filepath='%s', filename='%s' WHERE fid='%d'", $file->private, $file->filepath, $file->filename, $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); + $result = db_query("UPDATE {files} SET private='%d', filepath='%s', filename='%s' WHERE fid='%d'", $file->private, $file->filepath, $file->filename, $file->fid); + return TRUE; + } + else { + drupal_set_message(t('The chosen file could not be moved to the public path.'), 'error'); + } + } + + return FALSE; +} + +/** * @} End of "defgroup file". */ === modified file 'includes/locale.inc' --- includes/locale.inc 2008-05-26 17:12:54 +0000 +++ includes/locale.inc 2008-06-16 19:04:17 +0000 @@ -2117,11 +2117,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-05-13 18:13:43 +0000 +++ modules/blogapi/blogapi.module 2008-06-16 15:40:00 +0000 @@ -384,7 +384,7 @@ function blogapi_metaweblog_new_media_ob } // 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-04-14 17:48:33 +0000 +++ modules/locale/locale.install 2008-06-16 15:40:08 +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-05-10 07:32:02 +0000 +++ modules/system/system.admin.inc 2008-06-17 03:06:57 +0000 @@ -1443,6 +1443,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'), @@ -1454,10 +1463,10 @@ function system_file_system_settings() { $form['file_downloads'] = array( '#type' => 'radios', - '#title' => t('Download method'), + '#title' => t('Default 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.') + '#description' => t('Public files are accessible by everyone whereas private files are only accessible by those who can view the node the file is attached to.') ); return system_settings_form($form); === modified file 'modules/system/system.install' --- modules/system/system.install 2008-05-15 20:55:58 +0000 +++ modules/system/system.install 2008-06-17 04:59:05 +0000 @@ -663,6 +663,12 @@ function system_schema() { 'not null' => TRUE, 'default' => 0, ), + 'private' => array( + 'description' => t('A flag indicating whether file is public (1) or private (2).') + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), 'timestamp' => array( 'description' => t('UNIX timestamp for when the file was added.'), 'type' => 'int', @@ -679,6 +685,42 @@ function system_schema() { 'primary key' => array('fid'), ); + $schema['file_checkout'] = array( + 'description' => t('A table of private files which have been "checked-out" so that they can be downloaded for a certain period of time.'), + 'fields' => array( + 'checkout_id' => array( + 'description' => t('Primary Key: Unique checkout ID.'), + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'checkout_time' => array( + 'descritption' => t('UNIX timestamp for when the file was checked out.'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'key' => array( + 'description' => t('A unique key which allows a user to download a file for a period of time.'), + 'type' => 'char', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + ), + 'fid' => array( + 'description' => t('ID of file which the kde corresponds to.'), + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'indexes' = array( + 'key' => array('key'), + ), + 'primary key' => array('checkout_id'), + ), + ); + $schema['flood'] = array( 'description' => t('Flood controls the threshold of events, such as the number of contact attempts.'), 'fields' => array( @@ -2245,6 +2287,14 @@ function system_update_6029() { return array(array('success' => TRUE, 'query' => "'dblog' module enabled.")); } +function system_update_6030() { + $ret = array(); + + // Create files column + db_add_field($ret, 'files', 'private', array('type' => 'int', 'not null' => TRUE, 'default' => 0)); +} + + /** * Add the tables required by actions.inc. */ === modified file 'modules/system/system.module' --- modules/system/system.module 2008-05-07 19:17:50 +0000 +++ modules/system/system.module 2008-06-17 04:16:30 +0000 @@ -868,7 +868,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. * @@ -881,6 +881,32 @@ function system_check_directory($form_el } /** + * 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. * * @param $files === modified file 'modules/upload/upload.admin.inc' --- modules/upload/upload.admin.inc 2008-04-20 07:58:38 +0000 +++ modules/upload/upload.admin.inc 2008-06-16 15:40:26 +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-05-14 13:19:48 +0000 +++ modules/upload/upload.module 2008-06-16 17:43:04 +0000 @@ -146,13 +146,13 @@ function _upload_file_limits($user) { /** * Implementation of hook_file_download(). */ -function upload_file_download($filepath) { - $filepath = file_create_path($filepath); +function upload_file_download($file) { + if (!user_access('view uploaded files')) { + return -1; + } + $file = _file_create_path($file, FILE_DOWNLOADS_PRIVATE); $result = db_query("SELECT f.* FROM {files} f INNER JOIN {upload} u ON f.fid = u.fid WHERE filepath = '%s'", $file); if ($file = db_fetch_object($result)) { - if (!user_access('view uploaded files')) { - return -1; - } return array( 'Content-Type: ' . $file->filemime, 'Content-Length: ' . $file->filesize, @@ -179,6 +179,7 @@ function upload_node_form_submit($form, // Save new file uploads. if (($user->uid != 1 || user_access('upload files')) && ($file = file_save_upload('upload', $validators, file_directory_path()))) { + $file->private = variable_get('upload_private_default', FILE_DOWNLOADS_PUBLIC); $file->list = variable_get('upload_list_default', 1); $file->description = $file->filename; $file->weight = 0; @@ -334,7 +335,7 @@ function upload_nodeapi(&$node, $op, $te array( 'key' => 'enclosure', 'attributes' => array( - 'url' => file_create_url($file->filepath), + 'url' => file_create_url($file), 'length' => $file->filesize, 'type' => $file->filemime ) @@ -357,7 +358,7 @@ function theme_upload_attachments($files foreach ($files as $file) { $file = (object)$file; if ($file->list && empty($file->remove)) { - $href = file_create_url($file->filepath); + $href = file_create_url($file); $text = $file->description ? $file->description : $file->filename; $rows[] = array(l($text, $href), format_size($file->filesize)); } @@ -421,11 +422,13 @@ function upload_save(&$node) { if (!empty($node->old_vid) || isset($_SESSION['upload_files'][$fid])) { 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_status($file, FILE_STATUS_PERMANENT); + 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_status($file, FILE_STATUS_PERMANENT); + file_set_private($file); } } // Empty the session storage after save. We use this variable to track files @@ -480,11 +483,11 @@ function _upload_form($node) { $form['files']['#theme'] = 'upload_form_current'; $form['files']['#tree'] = TRUE; foreach ($node->files as $key => $file) { - $file = (object)$file; - $description = file_create_url($file->filepath); + $description = file_create_url($file); $description = "" . check_plain($description) . ""; $form['files'][$key]['description'] = array('#type' => 'textfield', '#default_value' => !empty($file->description) ? $file->description : $file->filename, '#maxlength' => 256, '#description' => $description ); $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); @@ -539,6 +542,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-05-26 17:12:54 +0000 +++ modules/user/user.module 2008-06-16 17:02:59 +0000 @@ -574,7 +574,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']); } } @@ -830,6 +830,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); } else if (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-06-16 20:19:08 +0000 @@ -0,0 +1,40 @@ +checkout_time + $expiration_time * 60; + + +// If the expiration time has not been reached we can transfer the file +if ($expiration_time > time()) { + file_transfer($file->filepath) +} +else { + drupal_access_denied(); +}