Commit 78b052a6 authored by Dries's avatar Dries

- The upload (filehandler) module has landed!
parent eeb2b17b
......@@ -207,6 +207,21 @@ CREATE TABLE directory (
PRIMARY KEY (link)
) TYPE=MyISAM;
--
-- Table structure for table 'files'
--
CREATE TABLE files (
fid int(10) unsigned NOT NULL default '0',
nid int(10) unsigned NOT NULL default '0',
filename varchar(255) NOT NULL default '',
filepath varchar(255) NOT NULL default '',
filemime varchar(255) NOT NULL default '',
filesize int(10) unsigned NOT NULL default '0',
list tinyint(1) unsigned NOT NULL default '0',
PRIMARY KEY (fid)
) TYPE=MyISAM;
--
-- Table structure for table 'filter_formats'
--
......
......@@ -208,6 +208,21 @@ CREATE TABLE directory (
PRIMARY KEY (link)
);
--
-- Table structure for table 'files'
--
CREATE TABLE files (
fid int(10) unsigned NOT NULL default '0',
nid int(10) unsigned NOT NULL default '0',
filename varchar(255) NOT NULL default '',
filepath varchar(255) NOT NULL default '',
filemime varchar(255) NOT NULL default '',
filesize int(10) unsigned NOT NULL default '0',
list tinyint(1) unsigned NOT NULL default '0',
PRIMARY KEY (fid)
);
--
-- Table structure for table 'filters'
--
......
......@@ -73,7 +73,8 @@
"2004-08-09" => "update_99",
"2004-08-10" => "update_100",
"2004-08-11" => "update_101",
"2004-08-12" => "update_102"
"2004-08-12" => "update_102",
"2004-08-17" => "update_103"
);
function update_32() {
......@@ -1506,6 +1507,21 @@ function update_102() {
return array(update_sql("INSERT INTO {system} (filename, name, type, description, status, throttle, bootstrap) VALUES ('modules/legacy.module', 'legacy', 'module', '', 1, 0, 0)"));
}
function update_103() {
$ret = array();
$ret[] = update_sql("CREATE TABLE files (
fid int(10) unsigned NOT NULL default '0',
nid int(10) unsigned NOT NULL default '0',
filename varchar(255) NOT NULL default '',
filepath varchar(255) NOT NULL default '',
filemime varchar(255) NOT NULL default '',
filesize int(10) unsigned NOT NULL default '0',
list tinyint(1) unsigned NOT NULL default '0',
PRIMARY KEY (fid)
)");
return $ret;
}
function update_sql($sql) {
$edit = $_POST["edit"];
$result = db_query($sql);
......
......@@ -1794,6 +1794,34 @@ function truncate_utf8($string, $len) {
return substr($string, 0, $len);
}
/**
* Encodes MIME/HTTP header values that contain non US-ASCII
* characters.
*
* Example: mime_header_encode('tést.txt') returns '=?UTF-8?B?dMOpc3QudHh0?='
*
* For more info: http://www.rfc-editor.org/rfc/rfc2047.txt
*
*/
function mime_header_encode($string, $charset = 'UTF-8') {
// Notes:
// - Only encode strings that contain non-ASCII characters.
// - The chunks come in groupings of 4 bytes when using base64
// encoded.
// - trim() is used to ensure that no extra spacing is added by
// chunk_split() or preg_replace().
// - Using \n as the chunk separator may cause problems on some
// systems and may have to be changed to \r\n or \r.
if (!preg_match('/^[\x20-\x7E]*$/', $string)) {
$chunk_size = 75 - 7 - strlen($charset);
$chunk_size -= $chunk_size % 4;
$string = trim(chunk_split(base64_encode($string), $chunk_size, "\n"));
$string = trim(preg_replace('/^(.*)$/m', " =?$charset?B?\\1?=", $string));
}
return $string;
}
/**
* Wrapper around PHP's eval(). Uses output buffering to capture both returned
* and printed text. Unlike eval(), we require code to be surrounded by <?php ?>
......
......@@ -9,7 +9,10 @@
define('FILE_DOWNLOADS_PUBLIC', 1);
define('FILE_DOWNLOADS_PRIVATE', 2);
define('FILE_SEPARATOR', PHP_OS == 'WINNT' ? '\\' : '/');
#define('FILE_SEPARATOR', PHP_OS == 'WINNT' ? '\\' : '/');
define('FILE_SEPARATOR', '/');
define('FILE_CREATE_DIRECTORY', 1);
define('FILE_MODIFY_PERMISSIONS', 2);
/**
* Create the download path to a file.
......@@ -20,8 +23,7 @@ function file_create_url($path) {
}
switch (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC)) {
case FILE_DOWNLOADS_PUBLIC:
global $base_url;
return $base_url .'/'. variable_get('file_directory_path', 'files') .'/'. str_replace('\\', '/', $path);
return $GLOBALS['base_url'] .'/'. variable_get('file_directory_path', 'files') .'/'. str_replace('\\', '/', $path);
case FILE_DOWNLOADS_PRIVATE:
return url('system/files', 'file='. $path);
}
......@@ -49,11 +51,38 @@ function file_create_path($dest = 0) {
* Check that directory exists and is writable.
*
* @param $directory Path to extract and verify directory for.
* @param $mode Try to create the directory if it does not exist.
* @param $form_item Optional name for a field item to attach potential errors to.
* @return False when directory not found, or true when directory exists.
*/
function file_check_directory(&$directory) {
function file_check_directory(&$directory, $mode = 0, $form_item = NULL) {
$directory = rtrim($directory, '/\\');
return is_dir($directory) && is_writable($directory);
// Check if directory exists.
if (!is_dir($directory)) {
if (($mode & FILE_CREATE_DIRECTORY) && mkdir($directory, 0760)) {
drupal_set_message(t("Created directory '%name'.", array('%name' => $directory)));
}
else {
if ($form_item) {
form_set_error($form_item, t("The directory '%name' does not exist.", array('%name' => $directory)));
}
return false;
}
}
// Check to see if the directory is writable.
if (!is_writable($directory)) {
if (($mode & FILE_MODIFY_PERMISSIONS) && chmod($directory, 0760)) {
drupal_set_message(t("Modified permissions on directory '%name'.", array('%name' => $directory)));
}
else {
form_set_error($form_item, t("The directory '%name' is not writable.", array('%name' => $directory)));
return false;
}
}
return true;
}
/**
......@@ -84,19 +113,25 @@ function file_check_path(&$path) {
*/
function file_check_upload($source) {
if (is_object($source)) {
if (is_file($source->path)) {
if (is_file($source->filepath)) {
return $source;
}
}
elseif ($_FILES["edit"]["name"][$source] && is_uploaded_file($_FILES["edit"]["tmp_name"][$source])) {
$file->name = trim(basename($_FILES["edit"]["name"][$source]), '.');
$file->type = $_FILES["edit"]["type"][$source];
$file->path = $_FILES["edit"]["tmp_name"][$source];
$file->filename = trim(basename($_FILES["edit"]["name"][$source]), '.');
$file->filemime = $_FILES["edit"]["type"][$source];
$file->filepath = $_FILES["edit"]["tmp_name"][$source];
$file->error = $_FILES["edit"]["error"][$source];
$file->size = $_FILES["edit"]["size"][$source];
$file->source = $_FILES["edit"]["source"][$source];
$file->filesize = $_FILES["edit"]["size"][$source];
$file->source = $source;
return $file;
}
else {
// In case of previews return previous file object.
if (file_exists($_SESSION['file_uploads'][$source]->filepath)) {
return $_SESSION['file_uploads'][$source];
}
}
}
/**
......@@ -154,9 +189,9 @@ function file_copy(&$source, $dest = 0, $replace = 0) {
// Process a file upload object.
if (is_object($source)) {
$file = $source;
$source = $file->path;
$source = $file->filepath;
if (!$basename) {
$basename = $file->name;
$basename = $file->filename;
}
}
......@@ -170,45 +205,53 @@ function file_copy(&$source, $dest = 0, $replace = 0) {
$basename = $basename ? $basename : basename($source);
$dest = $directory . FILE_SEPARATOR . $basename;
if (file_exists($dest) && !$replace) {
// Destination file already exists and we can't replace is so we try and
// and find a new filename.
$pos = strrpos($basename, '.');
$name = substr($basename, 0, $pos);
if ($pos = strrpos($basename, '.')) {
$name = substr($basename, 0, $pos);
$ext = substr($basename, $pos);
}
else {
$name = $basename;
}
// Make sure source and destination filenames are not the same, makes no sense
// to copy it if they are. In fact copying the file will most likely result in
// a 0 byte file. Which is bad. Real bad.
if ($source != realpath($dest)) {
if (file_exists($dest) && !$replace) {
// Destination file already exists and we can't replace is so we try and
// and find a new filename.
if ($pos = strrpos($basename, '.')) {
$name = substr($basename, 0, $pos);
$ext = substr($basename, $pos);
}
else {
$name = $basename;
}
$counter = 0;
do {
$dest = $directory . FILE_SEPARATOR . $name .'_'. $counter++ . $ext;
} while (file_exists($dest));
}
$counter = 0;
do {
$dest = $directory . FILE_SEPARATOR . $name .'_'. $counter++ . $ext;
} while (file_exists($dest));
}
if (!copy($source, $dest)) {
drupal_set_message(t('File copy failed.'), 'error');
return 0;
if (!copy($source, $dest)) {
drupal_set_message(t('File copy failed.'), 'error');
return 0;
}
}
if (is_object($file)) {
$file->name = $basename;
$file->path = $dest;
$file->filename = $basename;
$file->filepath = $dest;
$source = $file;
}
else {
$source = $dest;
}
return 1; // Everything went ok.
}
function file_move(&$source, $dest = 0, $replace = 0) {
$path = is_object($source) ? $source->path : $source;
$path_original = is_object($source) ? $source->filepath : $source;
if (file_copy($source, $dest, $replace)) {
if (unlink($path)) {
$path_current = is_object($source) ? $source->filepath : $source;
if ($path_original == $path_current || file_delete($path_original)) {
return 1;
}
drupal_set_message(t('Removing original file failed.'), 'error');
......@@ -216,9 +259,31 @@ function file_move(&$source, $dest = 0, $replace = 0) {
return 0;
}
function file_create_filename($basename, $directory) {
$dest = $directory . FILE_SEPARATOR . $basename;
if (file_exists($dest)) {
// Destination file already exists, generate an alternative.
if ($pos = strrpos($basename, '.')) {
$name = substr($basename, 0, $pos);
$ext = substr($basename, $pos);
}
else {
$name = $basename;
}
$counter = 0;
do {
$dest = $directory . FILE_SEPARATOR . $name .'_'. $counter++ . $ext;
} while (file_exists($dest));
}
return $dest;
}
function file_delete($path) {
if (is_file($path)) {
unlink($path);
return unlink($path);
}
}
......@@ -241,7 +306,7 @@ function file_save_upload($source, $dest = 0, $replace = 0) {
if (!$dest) {
$dest = variable_get('file_directory_temp', (PHP_OS == 'WINNT' ? 'c:\\windows\\temp' : '/tmp'));
$temporary = 1;
if (is_file($_SESSION['file_uploads'][$source]->path)) {
if (is_file($file->filepath)) {
// If this file was uploaded by this user before replace the temporary copy.
$replace = 1;
}
......@@ -273,18 +338,12 @@ function file_save_upload($source, $dest = 0, $replace = 0) {
unset($_SESSION['file_uploads'][is_object($source) ? $source->source : $source]);
if (file_move($file, $dest, $replace)) {
if ($temporary) {
$_SESSION['file_uploads'][$source] = $file;
$_SESSION['file_uploads'][is_object($source) ? $source->source : $source] = $file;
}
return $file;
}
return 0;
}
else {
// In case of previews return previous file object.
if (file_exists($_SESSION['file_uploads'][$source]->path)) {
return $_SESSION['file_uploads'][$source];
}
}
return 0;
}
......@@ -305,7 +364,7 @@ function file_save_data($data, $dest, $replace = 0) {
$temp = variable_get('file_directory_temp', (PHP_OS == 'WINNT' ? 'c:\\windows\\temp' : '/tmp'));
$file = tempnam($temp, 'file');
if (!$fp = fopen($file, 'w')) {
if (!$fp = fopen($file, 'wb')) {
drupal_set_message(t('Unable to create file.'), 'error');
return 0;
}
......@@ -378,7 +437,7 @@ function file_scan_directory($dir, $mask, $nomask = array('.', '..', 'CVS'), $ca
}
elseif (ereg($mask, $file)) {
$name = basename($file);
$files["$dir/$file"]->filename = "$dir/$file";
$files["$dir/$file"]->path = "$dir/$file";
$files["$dir/$file"]->name = substr($name, 0, strrpos($name, '.'));
if ($callback) {
$callback("$dir/$file");
......
......@@ -198,6 +198,10 @@ tr.light .form-item, tr.dark .form-item {
margin-right: 2em;
margin-bottom: 1em;
}
.node-form .attachments fieldset {
float: none;
display: block;
}
.ok {
color: #080;
}
......
......@@ -1127,6 +1127,9 @@ function node_form($edit) {
$output .= '<div class="node-form">';
// Add hidden 'op' variable, which specifies the default operation (Preview).
$output .= '<input type="hidden" name="op" value="'. check_form(t('Preview')) ."\" />\n";
// Add the admin-specific parts/
if (user_access('administer nodes')) {
......
......@@ -1127,6 +1127,9 @@ function node_form($edit) {
$output .= '<div class="node-form">';
// Add hidden 'op' variable, which specifies the default operation (Preview).
$output .= '<input type="hidden" name="op" value="'. check_form(t('Preview')) ."\" />\n";
// Add the admin-specific parts/
if (user_access('administer nodes')) {
......
......@@ -152,17 +152,13 @@ function system_view_general() {
// file system:
$directory_path = variable_get('file_directory_path', 'files');
if (!file_check_directory($directory_path)) {
form_set_error('file_directory_path', t("The directory '%name' does not exist, or is not writable.", array('%name' => $directory_path)));
}
file_check_directory($directory_path, FILE_CREATE_DIRECTORY, 'file_directory_path');
$directory_temp = variable_get('file_directory_temp', (PHP_OS == 'WINNT' ? 'c:\\windows\\temp' : '/tmp'));
if (!file_check_directory($directory_temp)) {
form_set_error('file_directory_temp', t("The directory '%name' does not exist, or is not writable.", array('%name' => $directory_temp)));
}
$directory_temp = variable_get('file_directory_temp', 'temp');
file_check_directory(file_create_path($directory_temp), FILE_CREATE_DIRECTORY, 'file_directory_temp');
$group = form_textfield(t('File system path'), 'file_directory_path', $directory_path, 70, 255, t('A file system path where the files will be stored. This directory has to exist and be writable by Drupal. If the download method is set to public this directory has to be relative to Drupal installation directory, and be accessible over the web. When download method is set to private this directory should not be accessible over the web. 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.'));
$group .= form_textfield(t('Temporary directory'), 'file_directory_temp', $directory_temp, 70, 255, t('Location where files can be saved temporarily. This directory should not be accessible from the web.'));
$group .= form_textfield(t('Temporary directory'), 'file_directory_temp', $directory_temp, 70, 255, t('Location where uploaded files will be placed for preview purposes. This directory should be relative to the file system path.'));
$group .= form_radios(t('Download method'), 'file_downloads', variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC), array(FILE_DOWNLOADS_PUBLIC => t('Public - files are available using http directly.'), FILE_DOWNLOADS_PRIVATE => t('Private - files are be transferred by Drupal.')), t('This setting can be changed at any time, however, all download URLs will change and there may be unexpected problems so it is not recommended.'));
$output .= form_group(t('File system settings'), $group);
......
......@@ -152,17 +152,13 @@ function system_view_general() {
// file system:
$directory_path = variable_get('file_directory_path', 'files');
if (!file_check_directory($directory_path)) {
form_set_error('file_directory_path', t("The directory '%name' does not exist, or is not writable.", array('%name' => $directory_path)));
}
file_check_directory($directory_path, FILE_CREATE_DIRECTORY, 'file_directory_path');
$directory_temp = variable_get('file_directory_temp', (PHP_OS == 'WINNT' ? 'c:\\windows\\temp' : '/tmp'));
if (!file_check_directory($directory_temp)) {
form_set_error('file_directory_temp', t("The directory '%name' does not exist, or is not writable.", array('%name' => $directory_temp)));
}
$directory_temp = variable_get('file_directory_temp', 'temp');
file_check_directory(file_create_path($directory_temp), FILE_CREATE_DIRECTORY, 'file_directory_temp');
$group = form_textfield(t('File system path'), 'file_directory_path', $directory_path, 70, 255, t('A file system path where the files will be stored. This directory has to exist and be writable by Drupal. If the download method is set to public this directory has to be relative to Drupal installation directory, and be accessible over the web. When download method is set to private this directory should not be accessible over the web. 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.'));
$group .= form_textfield(t('Temporary directory'), 'file_directory_temp', $directory_temp, 70, 255, t('Location where files can be saved temporarily. This directory should not be accessible from the web.'));
$group .= form_textfield(t('Temporary directory'), 'file_directory_temp', $directory_temp, 70, 255, t('Location where uploaded files will be placed for preview purposes. This directory should be relative to the file system path.'));
$group .= form_radios(t('Download method'), 'file_downloads', variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC), array(FILE_DOWNLOADS_PUBLIC => t('Public - files are available using http directly.'), FILE_DOWNLOADS_PRIVATE => t('Private - files are be transferred by Drupal.')), t('This setting can be changed at any time, however, all download URLs will change and there may be unexpected problems so it is not recommended.'));
$output .= form_group(t('File system settings'), $group);
......
<?php
/* $Id$ */
function upload_help($section) {
switch ($section) {
case 'admin/modules#description':
return t('File-handling and attaching files to nodes.');
case 'admin/upload':
return t('Users with the <a href="%permissions"><em>upload files</em> permission</a> can upload attachments. You can choose which node types can take attachments on the <a href="%workflow">workflow settings</a> page.', array('%permissions' => url('admin/user/configure/permission'), '%workflow' => url('admin/node/configure/defaults')));
case 'admin/node/configure/defaults':
return t('<p>If you want users to be able to attach files to nodes, check the <em>attachments</em> column in the appropriate column.</p>');
}
}
function upload_perm() {
return array('upload files');
}
function upload_menu() {
// Add handlers for previewing new uploads.
if ($_SESSION['file_uploads']) {
$items = array();
foreach ($_SESSION['file_uploads'] as $key => $file) {
$filename = file_create_filename($file->filename, file_create_path());
$items[] = array(
'path' => $filename, 'title' => t('file download'),
'callback' => 'upload_download',
'access' => true,
'type' => MENU_DYNAMIC_ITEM & MENU_HIDDEN
);
$_SESSION['file_uploads'][$key]->_filename = $filename;
}
}
$items[] = array(
'path' => 'admin/upload', 'title' => t('uploads'),
'callback' => 'upload_admin',
'access' => true,
'type' => MENU_NORMAL_ITEM
);
return $items;
}
function upload_admin() {
system_settings_save();
$group .= form_textfield(t("Maximum total file size"), "upload_maxsize_total", variable_get("upload_maxsize_total", 0), 5, 5, t("The maximum size of a file a user can upload in megabytes. Enter 0 for unlimited."));
$output = form_group(t('General settings'), $group);
$roles = user_roles(0, 'upload files');
foreach ($roles as $rid => $role) {
$group = form_textfield(t("Permitted file extensions"), "upload_extensions_$rid", variable_get("upload_extensions_$rid", "jpg jpeg gif png txt html doc xls pdf ppt pps"), 60, 255, t("Extensions that users in this role can upload. Separate extensions with a space and do not include the leading dot."));
$group .= form_textfield(t("Maximum file size per upload"), "upload_uploadsize_$rid", variable_get("upload_uploadsize_$rid", 1), 5, 5, t("The maximum size of a file a user can upload (in megabytes)."));
$group .= form_textfield(t("Total file size per user"), "upload_usersize_$rid", variable_get("upload_usersize_$rid", 10), 5, 5, t("The maximum size of all files a user can have on the site (in megabytes)."));
$output .= form_group(t("Settings for '%role'", array('%role' => $role)), $group);
}
print theme('page', system_settings_form($output));
}
function upload_download() {
foreach ($_SESSION['file_uploads'] as $file) {
if ($file->_filename == $_GET['q']) {
file_transfer($file->filepath, array('Content-Type: '. $file->filemime, 'Content-Length: '. $file->filesize));
}
}
}
function upload_file_download($file) {
$file = file_create_path($file);
$result = db_query("SELECT * from {files} WHERE filepath = '%s'", $file);
if ($file = db_fetch_object($result)) {
$name = mime_header_encode($file->filename);
// Serve images and text inline for the browser to display rather than download.
$disposition = ereg('^(text/|image/)', $file->filemime) ? 'inline' : 'attachment';
return array('Content-Type: '. $file->filemime .'; name='. $name,
'Content-Length: '. $file->filesize,
'Content-Disposition: '. $disposition .'; filename='. $name);
}
}
function upload_nodeapi(&$node, $op, $arg) {
switch ($op) {
case 'settings':
$output[t('attachments')] = form_checkbox(NULL, "upload_$node->type", 1, variable_get("upload_$node->type", 0));
break;
case 'form param':
if (variable_get("upload_$node->type", 0)) {
$output['options'] = array('enctype' => 'multipart/form-data');
}
break;
case 'validate':
$node->files = upload_load($node);
// Double check existing files:
if (is_array($node->list)) {
foreach ($node->list as $key => $value) {
if ($file = file_check_upload($key)) {
$node->files[$file->source] = $file;
$node->files[$key]->list = $node->list[$key];
$node->files[$key]->remove = $node->remove[$key];
if ($file->source) {
$filesize += $file->filesize;
}
}
}
}
else {
foreach ($node->files as $key => $file) {
$node->list[$key] = $file->list;
}
}
if ($file = file_check_upload('upload')) {
global $user;
$max_size = variable_get("upload_maxsize_total", 0);
$total_size = upload_count_size() + $filesize;
$total_usersize = upload_count_size($user->uid) + $filesize;
if ($maxsize && $total_size > $maxsize) {
drupal_set_message(t("Error attaching file '%name': total file size exceeded", array('%name' => $file->filename)), 'error');
break;
}
// Validate file against all users roles. Only denies an upload when
// all roles prevent it.
foreach ($user->roles as $rid => $name) {
$extensions = variable_get("upload_extensions_$rid", "jpg jpeg gif png txt html doc xls pdf ppt pps");
$uploadsize = variable_get("upload_uploadsize_$rid", 1);
$usersize = variable_get("upload_usersize_$rid", 1);
$regex = '/\.('. ereg_replace(' +', '|', preg_quote($extensions)) .')$/i';
if (!preg_match($regex, $file->filename)) {
$error['extension']++;
}
if ($file->filesize > $uploadsize * 1024 * 1024) {
$error['uploadsize']++;
}
if ($total_usersize + $file->filesize > $usersize * 1024 * 1024) {
$error['usersize']++;
}
}
if ($error['extension'] == count($user->roles)) {
drupal_set_message(t("error attaching file '%name': invalid extension", array('%name' => $file->filename)), 'error');
}
elseif ($error['uploadsize'] == count($user->roles)) {
drupal_set_message(t("error attaching file '%name': exceeds maximum file size", array('%name' => $file->filename)), 'error');
}
elseif ($error['usersize'] == count($user->roles)) {
drupal_set_message(t("error attaching file '%name': exceeds maximum file size", array('%name' => $file->filename)), 'error');
}