upload.module 33.4 KB
Newer Older
Dries's avatar
 
Dries committed
1
<?php
Dries's avatar
Dries committed
2
// $Id$
Dries's avatar
 
Dries committed
3

Dries's avatar
   
Dries committed
4
5
6
/**
 * @file
 * File-handling and attaching files to nodes.
7
 *
Dries's avatar
   
Dries committed
8
9
 */

10
11
12
/**
 * Implementation of hook_help().
 */
Dries's avatar
 
Dries committed
13
14
function upload_help($section) {
  switch ($section) {
15
    case 'admin/help#upload':
16
17
      $output = '<p>'. t('The upload module allows users to upload files to the site. The ability to upload files to a site is important for members of a community who want to share work. It is also useful to administrators who want to keep uploaded files connected to a node or page.') .'</p>';
      $output .= '<p>'. t('Users with the upload files permission can upload attachments. You can choose which post types can take attachments on the content types settings page. Each user role can be customized for the file size of uploads, and the dimension of image files.') .'</p>';
18
      $output .= '<p>'. t('For more information please read the configuration and customization handbook <a href="@upload">Upload page</a>.', array('@upload' => 'http://drupal.org/handbook/modules/upload/')) .'</p>';
19
      return $output;
20
    case 'admin/settings/upload':
21
      return '<p>'. t('Users with the <a href="@permissions">upload files permission</a> can upload attachments. Users with the <a href="@permissions">view uploaded files permission</a> can view uploaded attachments. You can choose which post types can take attachments on the <a href="@types">content types settings</a> page.', array('@permissions' => url('admin/user/access'), '@types' => url('admin/settings/types'))) .'</p>';
Dries's avatar
 
Dries committed
22
23
24
  }
}

25
26
27
/**
 * Implementation of hook_perm().
 */
Dries's avatar
 
Dries committed
28
function upload_perm() {
29
  return array('upload files', 'view uploaded files');
Dries's avatar
 
Dries committed
30
31
}

32
33
34
/**
 * Implementation of hook_link().
 */
35
function upload_link($type, $node = NULL, $teaser = FALSE) {
36
37
38
  $links = array();

  // Display a link with the number of attachments
39
  if ($teaser && $type == 'node' && isset($node->files) && user_access('view uploaded files')) {
40
41
42
43
44
45
46
    $num_files = 0;
    foreach ($node->files as $file) {
      if ($file->list) {
        $num_files++;
      }
    }
    if ($num_files) {
47
      $links['upload_attachments'] = array(
48
        'title' => format_plural($num_files, '1 attachment', '@count attachments'),
49
50
51
        'href' => "node/$node->nid",
        'attributes' => array('title' => t('Read full article to view attachments.')),
        'fragment' => 'attachments'
52
      );
53
54
55
56
57
58
59
60
61
    }
  }

  return $links;
}

/**
 * Implementation of hook_menu().
 */
Dries's avatar
   
Dries committed
62
63
64
65
function upload_menu($may_cache) {
  $items = array();

  if ($may_cache) {
66
67
68
69
70
71
    $items[] = array(
      'path' => 'upload/js',
      'callback' => 'upload_js',
      'access' => user_access('upload files'),
      'type' => MENU_CALLBACK
    );
Dries's avatar
Dries committed
72
    $items[] = array('path' => 'admin/settings/uploads',
73
      'title' => t('File uploads'),
74
      'description' => t('Control how files may be attached to content.'),
75
76
      'callback' => 'drupal_get_form',
      'callback arguments' => array('upload_admin_settings'),
Dries's avatar
Dries committed
77
78
      'access' => user_access('administer site configuration'),
      'type' => MENU_NORMAL_ITEM);
79
80
  }
  else {
Dries's avatar
   
Dries committed
81
    // Add handlers for previewing new uploads.
82
    if (isset($_SESSION['file_previews'])) {
83
      foreach ($_SESSION['file_previews'] as $fid => $file) {
Dries's avatar
   
Dries committed
84
        $filename = file_create_filename($file->filename, file_create_path());
85
        if (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) ==  FILE_DOWNLOADS_PRIVATE) {
Dries's avatar
Dries committed
86
          // strip file_directory_path() from filename. @see file_create_url
87
          if (strpos($filename, file_directory_path()) !== FALSE) {
drumm's avatar
drumm committed
88
89
90
            $filename = trim(substr($filename, strlen(file_directory_path())), '\\/');
          }
          $filename = 'system/files/' . $filename;
91
92
        }

Dries's avatar
   
Dries committed
93
        $items[] = array(
94
          'path' => $filename, 'title' => t('File download'),
Dries's avatar
   
Dries committed
95
          'callback' => 'upload_download',
96
          'access' => user_access('view uploaded files'),
97
          'type' => MENU_CALLBACK
Dries's avatar
   
Dries committed
98
        );
99
        $_SESSION['file_previews'][$fid]->_filename = $filename;
Dries's avatar
   
Dries committed
100
      }
Dries's avatar
 
Dries committed
101
102
    }
  }
Dries's avatar
   
Dries committed
103

Dries's avatar
 
Dries committed
104
105
106
  return $items;
}

107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/**
 * Form API callback to validate the upload settings form.
 */
function upload_admin_settings_validate($form_id, $form_values) {
  if (($form_values['upload_max_resolution'] != '0')) {
    if (!preg_match('/^[0-9]+x[0-9]+$/', $form_values['upload_max_resolution'])) {
      form_set_error('upload_max_resolution', t('The maximum allowed image size expressed as WIDTHxHEIGHT (e.g. 640x480). Set to 0 for no restriction.'));
    }
  }

  $default_uploadsize = $form_values['upload_uploadsize_default'];
  $default_usersize = $form_values['upload_usersize_default'];

  $exceed_max_msg = t('Your PHP settings limit the maximum file size per upload to %size MB.', array('%size' => file_upload_max_size())).'<br/>';
  $more_info = t("Depending on your sever environment, these settings may be changed in the system-wide php.ini file, a php.ini file in your Drupal root directory, in your Drupal site's settings.php file, or in the .htaccess file in your Drupal root directory.");

  if (!is_numeric($default_uploadsize) || ($default_uploadsize <= 0)) {
    form_set_error('upload_uploadsize_default', t('The %role file size limit must be a number and greater than zero.', array('%role' => t('default'))));
  }
  if (!is_numeric($default_usersize) || ($default_usersize <= 0)) {
    form_set_error('upload_usersize_default', t('The %role file size limit must be a number and greater than zero.', array('%role' => t('default'))));
  }
  if ($default_uploadsize > file_upload_max_size()) {
   form_set_error('upload_uploadsize_default', $exceed_max_msg . $more_info);
   $more_info = '';
  }
  if ($default_uploadsize > $default_usersize) {
   form_set_error('upload_uploadsize_default', t('The %role maximum file size per upload is greater than the total file size allowed per user', array('%role' => t('default'))));
  }

  foreach ($form_values['roles'] as $rid => $role) {
    $uploadsize = $form_values['upload_uploadsize_'. $rid];
    $usersize = $form_values['upload_usersize_'. $rid];

    if (!is_numeric($uploadsize) || ($uploadsize <= 0)) {
142
      form_set_error('upload_uploadsize_'. $rid, t('The %role file size limit must be a number and greater than zero.', array('%role' => $role)));
143
144
    }
    if (!is_numeric($usersize) || ($usersize <= 0)) {
145
      form_set_error('upload_usersize_'. $rid, t('The %role file size limit must be a number and greater than zero.', array('%role' => $role)));
146
147
148
149
150
151
    }
    if ($uploadsize > file_upload_max_size()) {
     form_set_error('upload_uploadsize_'. $rid, $exceed_max_msg . $more_info);
     $more_info = '';
    }
    if ($uploadsize > $usersize) {
152
     form_set_error('upload_uploadsize_'. $rid, t('The %role maximum file size per upload is greater than the total file size allowed per user', array('%role' => $role)));
153
154
155
156
157
158
159
    }
  }
}

/**
 * Menu callback for the upload settings form.
 */
Dries's avatar
Dries committed
160
function upload_admin_settings() {
161
  $upload_extensions_default = variable_get('upload_extensions_default', 'jpg jpeg gif png txt html doc xls pdf ppt pps odt ods odp');
162
  $upload_uploadsize_default = variable_get('upload_uploadsize_default', 1);
163
  $upload_usersize_default = variable_get('upload_usersize_default', 1);
164
165

  $form['settings_general'] = array(
166
167
    '#type' => 'fieldset',
    '#title' => t('General settings'),
168
169
    '#collapsible' => TRUE,
  );
170
  $form['settings_general']['upload_max_resolution'] = array(
171
172
    '#type' => 'textfield',
    '#title' => t('Maximum resolution for uploaded images'),
173
    '#default_value' => variable_get('upload_max_resolution', 0),
174
175
    '#size' => 15,
    '#maxlength' => 10,
176
    '#description' => t('The maximum allowed image size (e.g. 640x480). Set to 0 for no restriction.'),
177
    '#field_suffix' => '<kbd>'. t('WIDTHxHEIGHT') .'</kbd>'
178
  );
179
  $form['settings_general']['upload_list_default'] = array(
180
    '#type' => 'select',
181
182
183
    '#title' => t('List files by default'),
    '#default_value' => variable_get('upload_list_default', 1),
    '#options' => array(0 => t('No'), 1 => t('Yes')),
184
185
    '#description' => t('Set whether files attached to nodes are listed or not in the node view by default.'),
  );
186

187
  $form['settings_general']['upload_extensions_default'] = array(
188
189
    '#type' => 'textfield',
    '#title' => t('Default permitted file extensions'),
190
    '#default_value' => $upload_extensions_default,
191
    '#maxlength' => 255,
192
193
194
    '#description' => t('Default extensions that users can upload. Separate extensions with a space and do not include the leading dot.'),
  );
  $form['settings_general']['upload_uploadsize_default'] = array(
195
196
    '#type' => 'textfield',
    '#title' => t('Default maximum file size per upload'),
197
    '#default_value' => $upload_uploadsize_default,
198
199
    '#size' => 5,
    '#maxlength' => 5,
200
201
    '#description' => t('The default maximum file size a user can upload.'),
    '#field_suffix' => t('MB')
202
203
  );
  $form['settings_general']['upload_usersize_default'] = array(
204
205
    '#type' => 'textfield',
    '#title' => t('Default total file size per user'),
206
    '#default_value' => $upload_usersize_default,
207
208
    '#size' => 5,
    '#maxlength' => 5,
209
210
    '#description' => t('The default maximum size of all files a user can have on the site.'),
    '#field_suffix' => t('MB')
211
212
  );

213
  $form['settings_general']['upload_max_size'] = array('#value' => '<p>'. t('Your PHP settings limit the maximum file size per upload to %size.', array('%size' => format_size(file_upload_max_size()))).'</p>');
214

Dries's avatar
 
Dries committed
215
  $roles = user_roles(0, 'upload files');
216
  $form['roles'] = array('#type' => 'value', '#value' => $roles);
Dries's avatar
 
Dries committed
217
218

  foreach ($roles as $rid => $role) {
219
    $form['settings_role_'. $rid] = array(
220
      '#type' => 'fieldset',
221
      '#title' => t('Settings for @role', array('@role' => $role)),
222
      '#collapsible' => TRUE,
223
224
      '#collapsed' => TRUE,
    );
225
    $form['settings_role_'. $rid]['upload_extensions_'. $rid] = array(
226
227
      '#type' => 'textfield',
      '#title' => t('Permitted file extensions'),
228
      '#default_value' => variable_get('upload_extensions_'. $rid, $upload_extensions_default),
229
      '#maxlength' => 255,
230
      '#description' => t('Extensions that users in this role can upload. Separate extensions with a space and do not include the leading dot.'),
231
    );
232
    $form['settings_role_'. $rid]['upload_uploadsize_'. $rid] = array(
233
234
      '#type' => 'textfield',
      '#title' => t('Maximum file size per upload'),
235
      '#default_value' => variable_get('upload_uploadsize_'. $rid, $upload_uploadsize_default),
236
237
      '#size' => 5,
      '#maxlength' => 5,
238
      '#description' => t('The maximum size of a file a user can upload (in megabytes).'),
239
    );
240
    $form['settings_role_'. $rid]['upload_usersize_'. $rid] = array(
241
242
      '#type' => 'textfield',
      '#title' => t('Total file size per user'),
243
      '#default_value' => variable_get('upload_usersize_'. $rid, $upload_usersize_default),
244
245
      '#size' => 5,
      '#maxlength' => 5,
246
      '#description' => t('The maximum size of all files a user can have on the site (in megabytes).'),
247
    );
Dries's avatar
 
Dries committed
248
249
  }

250
  return system_settings_form($form);
Dries's avatar
 
Dries committed
251
252
253
}

function upload_download() {
254
  foreach ($_SESSION['file_previews'] as $file) {
Dries's avatar
 
Dries committed
255
    if ($file->_filename == $_GET['q']) {
Dries's avatar
Dries committed
256
      file_transfer($file->filepath, array('Content-Type: '. mime_header_encode($file->filemime), 'Content-Length: '. $file->filesize));
Dries's avatar
 
Dries committed
257
258
259
260
261
    }
  }
}

function upload_file_download($file) {
262
263
264
265
  $file = file_create_path($file);
  $result = db_query("SELECT f.* FROM {files} f WHERE filepath = '%s'", $file);
  if ($file = db_fetch_object($result)) {
    if (user_access('view uploaded files')) {
266
267
268
      $node = node_load($file->nid);
      if (node_access('view', $node)) {
        $type = mime_header_encode($file->filemime);
drumm's avatar
drumm committed
269
        return array(
270
          'Content-Type: '. $type,
drumm's avatar
drumm committed
271
272
          'Content-Length: '. $file->filesize,
        );
273
      }
274
275
276
      else {
        return -1;
      }
277
    }
278
279
280
    else {
      return -1;
    }
281
  }
Dries's avatar
 
Dries committed
282
283
}

284
/**
285
286
287
288
289
290
291
292
293
 * Save new uploads and attach them to the node object.
 * append file_previews to the node object as well.
 */
function _upload_prepare(&$node) {

  // Clean up old file previews if a post didn't get the user to this page.
  // i.e. the user left the edit page, because they didn't want to upload anything.
  if(count($_POST) == 0) {
    if (is_array($_SESSION['file_previews']) && count($_SESSION['file_previews'])) {
294
      foreach ($_SESSION['file_previews'] as $fid => $file) {
drumm's avatar
drumm committed
295
        file_delete($file->filepath);
296
297
298
299
300
      }
      unset($_SESSION['file_previews']);
    }
  }

301
  // $_SESSION['file_current_upload'] tracks the fid of the file submitted this page request.
302
303
304
305
306
  // form_builder sets the value of file->list to 0 for checkboxes added to a form after
  // it has been submitted. Since unchecked checkboxes have no return value and do not
  // get a key in _POST form_builder has no way of knowing the difference between a check
  // box that wasn't present on the last form build, and a checkbox that is unchecked.

307
  unset($_SESSION['file_current_upload']);
308

Gerhard Killesreiter's avatar
Gerhard Killesreiter committed
309
310
  global $user;

311
312
313
314
315
316
317
318
319
320
321
322
323
324
  // Save new file uploads to tmp dir.
  if (($file = file_check_upload()) && user_access('upload files')) {

    // Scale image uploads.
    $file = _upload_image($file);

    $key = 'upload_'. count($_SESSION['file_previews']);
    $file->fid = $key;
    $file->source = $key;
    $file->list = variable_get('upload_list_default',1);
    $_SESSION['file_previews'][$key] = $file;

    // Store the uploaded fid for this page request in case of submit without
    // preview or attach. See earlier notes.
325
    $_SESSION['file_current_upload'] = $key;
326
327
328
329
  }

  // Attach file previews to node object.
  if (is_array($_SESSION['file_previews']) && count($_SESSION['file_previews'])) {
330
    foreach ($_SESSION['file_previews'] as $fid => $file) {
Gerhard Killesreiter's avatar
Gerhard Killesreiter committed
331
332
333
334
335
      if ($user->uid != 1) {
        // Here something.php.pps becomes something.php_.pps
        $file->filename = upload_munge_filename($file->filename, NULL, 0);
        $file->description = $file->filename;
      }
336
337
338
339
340
      $node->files[$fid] = $file;
    }
  }
}

341
function upload_form_alter($form_id, &$form) {
342
343
344
345
  if ($form_id == 'node_type_form' && isset($form['identity']['type'])) {
    $form['workflow']['upload'] = array(
      '#type' => 'radios',
      '#title' => t('Attachments'),
346
      '#default_value' => variable_get('upload_'. $form['#node_type']->type, 1),
347
348
349
      '#options' => array(t('Disabled'), t('Enabled')),
    );
  }
350

351
  if (isset($form['type'])) {
352
    $node = $form['#node'];
353
    if ($form['type']['#value'] .'_node_form' == $form_id && variable_get("upload_$node->type", TRUE)) {
354
355
      drupal_add_js('misc/progress.js');
      drupal_add_js('misc/upload.js');
356

357
      // Attachments fieldset
358
359
      $form['attachments'] = array(
        '#type' => 'fieldset',
360
        '#access' => user_access('upload files'),
361
362
363
364
365
366
        '#title' => t('File attachments'),
        '#collapsible' => TRUE,
        '#collapsed' => empty($node->files),
        '#description' => t('Changes made to the attachments are not permanent until you save this post. The first "listed" file will be included in RSS feeds.'),
        '#prefix' => '<div class="attachments">',
        '#suffix' => '</div>',
367
        '#weight' => 30,
368
      );
369
370
371
372
373
374

      // Wrapper for fieldset contents (used by upload JS).
      $form['attachments']['wrapper'] = array(
        '#prefix' => '<div id="attach-wrapper">',
        '#suffix' => '</div>',
      );
375
376
377

      // Make sure necessary directories for upload.module exist and are
      // writable before displaying the attachment form.
378
379
380
381
      $path = file_directory_path();
      $temp = file_directory_temp();
      // Note: pass by reference
      if (!file_check_directory($path, FILE_CREATE_DIRECTORY) || !file_check_directory($temp, FILE_CREATE_DIRECTORY)) {
382
383
384
385
386
387
388
389
390
391
392
393
        $form['attachments']['#description'] =  t('File attachments are disabled. The file directories have not been properly configured.');
        if (user_access('administer site configuration')) {
          $form['attachments']['#description'] .= ' '. t('Please visit the <a href="@admin-file-system">file system configuration page</a>.', array('@admin-file-system' => url('admin/settings/file-system')));
        }
        else {
          $form['attachments']['#description'] .= ' '. t('Please contact the site administrator.');
        }
      }
      else {
        $form['attachments']['wrapper'] += _upload_form($node);
        $form['#attributes']['enctype'] = 'multipart/form-data';
      }
394
    }
395
396
397
  }
}

398
function _upload_validate(&$node) {
399
400
401
402
  // Accumulator for disk space quotas.
  $filesize = 0;

  // Check if node->files exists, and if it contains something.
403
  if (is_array($node->files)) {
404
    // Update existing files with form data.
405
    foreach ($node->files as $fid => $file) {
406
      // Convert file to object for compatibility
407
      $file = (object)$file;
408

409
      // Validate new uploads.
410
      if (strpos($fid, 'upload') !== FALSE && !$file->remove) {
Dries's avatar
 
Dries committed
411
412
        global $user;

413
        // Bypass validation for uid  = 1.
Steven Wittens's avatar
Steven Wittens committed
414
        if ($user->uid != 1) {
415
          // Update filesize accumulator.
416
417
418
419
420
          $filesize += $file->filesize;

          // Validate file against all users roles.
          // Only denies an upload when all roles prevent it.

421
          $total_usersize = upload_space_used($user->uid) + $filesize;
422
          $error = array();
Steven Wittens's avatar
Steven Wittens committed
423
          foreach ($user->roles as $rid => $name) {
424
            $extensions = variable_get("upload_extensions_$rid", variable_get('upload_extensions_default', 'jpg jpeg gif png txt html doc xls pdf ppt pps odt ods odp'));
425
            $uploadsize = variable_get("upload_uploadsize_$rid", variable_get('upload_uploadsize_default', 1)) * 1024 * 1024;
426
            $usersize = variable_get("upload_usersize_$rid", variable_get('upload_usersize_default', 1)) * 1024 * 1024;
Steven Wittens's avatar
Steven Wittens committed
427
428
429
430
431
432

            $regex = '/\.('. ereg_replace(' +', '|', preg_quote($extensions)) .')$/i';

            if (!preg_match($regex, $file->filename)) {
              $error['extension']++;
            }
Dries's avatar
 
Dries committed
433

434
            if ($uploadsize && $file->filesize > $uploadsize) {
Steven Wittens's avatar
Steven Wittens committed
435
436
              $error['uploadsize']++;
            }
Dries's avatar
 
Dries committed
437

438
            if ($usersize && $total_usersize + $file->filesize > $usersize) {
Steven Wittens's avatar
Steven Wittens committed
439
440
              $error['usersize']++;
            }
Dries's avatar
 
Dries committed
441
          }
442
443
444
445

          $user_roles = count($user->roles);
          $valid = TRUE;
          if ($error['extension'] == $user_roles) {
446
            form_set_error('upload', t('The selected file %name can not be attached to this post, because it is only possible to attach files with the following extensions: %files-allowed.', array('%name' => $file->filename, '%files-allowed' => $extensions)));
447
            $valid = FALSE;
448
          }
449
          elseif ($error['uploadsize'] == $user_roles) {
450
            form_set_error('upload', t('The selected file %name can not be attached to this post, because it exceeded the maximum filesize of %maxsize.', array('%name' => $file->filename, '%maxsize' => format_size($uploadsize))));
451
            $valid = FALSE;
452
          }
453
          elseif ($error['usersize'] == $user_roles) {
454
            form_set_error('upload', t('The selected file %name can not be attached to this post, because the disk quota of %quota has been reached.', array('%name' => $file->filename, '%quota' => format_size($usersize))));
455
            $valid = FALSE;
456
          }
457
          elseif (strlen($file->filename) > 255) {
458
            form_set_error('upload', t('The selected file %name can not be attached to this post, because the filename is too long.', array('%name' => $file->filename)));
Gerhard Killesreiter's avatar
Gerhard Killesreiter committed
459
460
461
            $valid = FALSE;
          }

462
          if (!$valid) {
463
464
465
            unset($node->files[$fid], $_SESSION['file_previews'][$fid]);
            file_delete($file->filepath);
          }
466
        }
467
      }
468
469
470
471
472
473
474
    }
  }
}

/**
 * Implementation of hook_nodeapi().
 */
475
function upload_nodeapi(&$node, $op, $teaser) {
476
  switch ($op) {
477

Dries's avatar
 
Dries committed
478
    case 'load':
drumm's avatar
drumm committed
479
      $output = '';
480
      if (variable_get("upload_$node->type", 1) == 1) {
Steven Wittens's avatar
Steven Wittens committed
481
        $output['files'] = upload_load($node);
482
        return $output;
Dries's avatar
 
Dries committed
483
      }
484
485
486
487
488
489
      break;

    case 'prepare':
      _upload_prepare($node);
      break;

490
491
492
493
    case 'validate':
      _upload_validate($node);
      break;

Dries's avatar
 
Dries committed
494
    case 'view':
495
      if (isset($node->files) && user_access('view uploaded files')) {
496
497
498
499
500
501
502
503
        // Add the attachments list to node body with a heavy
        // weight to ensure they're below other elements
        if (count($node->files)) {
          if (!$teaser && user_access('view uploaded files')) {
            $node->content['files'] = array(
              '#value' => theme('upload_attachments', $node->files),
              '#weight' => 50,
            );
Dries's avatar
 
Dries committed
504
          }
505
506
507
508
509
510
511
512
513
        }
      }
      break;
    case 'alter':
      if (isset($node->files) && user_access('view uploaded files')) {
        // Manipulate so that inline references work in preview
        if (!variable_get('clean_url', 0)) {
          $previews = array();
          foreach ($node->files as $file) {
514
            if (strpos($file->fid, 'upload') !== FALSE) {
515
              $previews[] = $file;
516
            }
517
518
519
520
521
522
523
524
          }

          // URLs to files being previewed are actually Drupal paths. When Clean
          // URLs are disabled, the two do not match. We perform an automatic
          // replacement from temporary to permanent URLs. That way, the author
          // can use the final URL in the body before having actually saved (to
          // place inline images for example).
          foreach ($previews as $file) {
525
            $old = file_create_filename($file->filename, file_create_path());
526
527
528
            $new = url($old);
            $node->body = str_replace($old, $new, $node->body);
            $node->teaser = str_replace($old, $new, $node->teaser);
Dries's avatar
 
Dries committed
529
530
531
532
          }
        }
      }
      break;
533
534
535
536
537
538
539
540
541
542
543
    case 'insert':
    case 'update':
      if (user_access('upload files')) {
        upload_save($node);
      }
      break;

    case 'delete':
      upload_delete($node);
      break;

544
545
546
    case 'delete revision':
      upload_delete_revision($node);
      break;
547

Dries's avatar
Dries committed
548
    case 'search result':
549
      return is_array($node->files) ? format_plural(count($node->files), '1 attachment', '@count attachments') : NULL;
550

Dries's avatar
   
Dries committed
551
    case 'rss item':
552
      if (is_array($node->files)) {
Dries's avatar
   
Dries committed
553
554
555
556
557
558
559
560
561
        $files = array();
        foreach ($node->files as $file) {
          if ($file->list) {
            $files[] = $file;
          }
        }
        if (count($files) > 0) {
          // RSS only allows one enclosure per item
          $file = array_shift($files);
drumm's avatar
drumm committed
562
563
564
565
566
567
568
569
570
571
          return array(
            array(
              'key' => 'enclosure',
              'attributes' => array(
                'url' => file_create_url($file->filepath),
                'length' => $file->filesize,
                'type' => $file->filemime
              )
            )
          );
Dries's avatar
   
Dries committed
572
573
        }
      }
574
      return array();
575
576
  }
}
Dries's avatar
 
Dries committed
577

578
579
580
581
582
583
584
585
/**
 * Displays file attachments in table
 */
function theme_upload_attachments($files) {
  $header = array(t('Attachment'), t('Size'));
  $rows = array();
  foreach ($files as $file) {
    if ($file->list) {
586
587
      $href = $file->fid ? file_create_url($file->filepath) : url(file_create_filename($file->filename, file_create_path()));
      $text = $file->description ? $file->description : $file->filename;
588
589
590
591
592
      $rows[] = array(l($text, $href), format_size($file->filesize));
    }
  }
  if (count($rows)) {
    return theme('table', $header, $rows, array('id' => 'attachments'));
593
  }
Dries's avatar
 
Dries committed
594
595
}

596
597
598
599
600
601
/**
 * Determine how much disk space is occupied by a user's uploaded files.
 *
 * @param $uid
 *   The integer user id of a user.
 * @return
602
 *   The amount of disk space used by the user in bytes.
603
604
 */
function upload_space_used($uid) {
605
  return db_result(db_query('SELECT SUM(filesize) FROM {files} f INNER JOIN {node} n ON f.nid = n.nid WHERE n.uid = %d', $uid));
606
}
Dries's avatar
 
Dries committed
607

608
609
610
611
/**
 * Determine how much disk space is occupied by uploaded files.
 *
 * @return
612
 *   The amount of disk space used by uploaded files in bytes.
613
614
 */
function upload_total_space_used() {
615
  return db_result(db_query('SELECT SUM(filesize) FROM {files}'));
Dries's avatar
 
Dries committed
616
617
}

Gerhard Killesreiter's avatar
Gerhard Killesreiter committed
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
/**
 * Munge the filename as needed for security purposes.
 *
 * @param $filename
 *   The name of a file to modify.
 * @param $extensions
 *   A space separated list of valid extensions. If this is blank, we'll use
 *   the admin-defined defaults for the user role from upload_extensions_$rid.
 * @param $alerts
 *   Whether alerts (watchdog, drupal_set_message()) should be displayed.
 * @return $filename
 *   The potentially modified $filename.
 */
function upload_munge_filename($filename, $extensions = NULL, $alerts = 1) {
  global $user;

  $original = $filename;

  // Allow potentially insecure uploads for very savvy users and admin
  if (!variable_get('allow_insecure_uploads', 0)) {

    if (!isset($extensions)) {
      $extensions = '';
      foreach ($user->roles as $rid => $name) {
642
        $extensions .= ' '. variable_get("upload_extensions_$rid", variable_get('upload_extensions_default', 'jpg jpeg gif png txt html doc xls pdf ppt pps odt ods odp'));
Gerhard Killesreiter's avatar
Gerhard Killesreiter committed
643
644
645
646
647
648
649
650
651
652
653
      }

    }

    $whitelist = array_unique(explode(' ', trim($extensions)));

    $filename_parts = explode('.', $filename);

    $new_filename = array_shift($filename_parts); // Remove file basename.
    $final_extension = array_pop($filename_parts); // Remove final extension.

654
    foreach ($filename_parts as $filename_part) {
Gerhard Killesreiter's avatar
Gerhard Killesreiter committed
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
      $new_filename .= ".$filename_part";
      if (!in_array($filename_part, $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
        $new_filename .= '_';
      }
    }
    $filename = "$new_filename.$final_extension";
  }

  if ($alerts && $original != $filename) {
    $message = t('Your filename has been renamed to conform to site policy.');
    drupal_set_message($message);
  }

  return $filename;
}

/**
 * Undo the effect of upload_munge_filename().
 */
function upload_unmunge_filename($filename) {
  return str_replace('_.', '.', $filename);
}

678
function upload_save(&$node) {
679
680
681
682
  if (!is_array($node->files)) {
    return;
  }

683
  foreach ($node->files as $fid => $file) {
684
    // Convert file to object for compatibility
685
686
687
688
689
690
    $file = (object)$file;

    // Remove file. Process removals first since no further processing
    // will be required.
    if ($file->remove) {
      // Remove file previews...
691
      if (strpos($file->fid, 'upload') !== FALSE) {
drumm's avatar
drumm committed
692
        file_delete($file->filepath);
Dries's avatar
 
Dries committed
693
      }
694

695
696
697
      // Remove managed files.
      else {
        db_query('DELETE FROM {file_revisions} WHERE fid = %d AND vid = %d', $fid, $node->vid);
698
        // Only delete a file if it isn't used by any revision
699
        $count = db_result(db_query('SELECT COUNT(fid) FROM {file_revisions} WHERE fid = %d', $fid));
700
        if ($count < 1) {
701
          db_query('DELETE FROM {files} WHERE fid = %d', $fid);
702
703
          file_delete($file->filepath);
        }
Dries's avatar
 
Dries committed
704
      }
705
    }
706

707
    // New file upload
708
    elseif (strpos($file->fid, 'upload') !== FALSE) {
709
710
711
712
      if ($file = file_save_upload($file, $file->filename)) {
        $file->fid = db_next_id('{files}_fid');
        db_query("INSERT INTO {files} (fid, nid, filename, filepath, filemime, filesize) VALUES (%d, %d, '%s', '%s', '%s', %d)", $file->fid, $node->nid, $file->filename, $file->filepath, $file->filemime, $file->filesize);
        db_query("INSERT INTO {file_revisions} (fid, vid, list, description) VALUES (%d, %d, %d, '%s')", $file->fid, $node->vid, $file->list, $file->description);
713
714
        // Tell other modules where the file was stored.
        $node->files[$fid] = $file;
715
      }
716
717
718
719
720
721
722
723
724
725
726
      unset($_SESSION['file_previews'][$fid]);
    }

    // Create a new revision, as needed
    elseif ($node->old_vid && is_numeric($fid)) {
      db_query("INSERT INTO {file_revisions} (fid, vid, list, description) VALUES (%d, %d, %d, '%s')", $file->fid, $node->vid, $file->list, $file->description);
    }

    // Update existing revision
    else {
      db_query("UPDATE {file_revisions} SET list = %d, description = '%s' WHERE fid = %d AND vid = %d", $file->list, $file->description, $file->fid, $node->vid);
727
728
    }
  }
Dries's avatar
 
Dries committed
729
730
}

731
function upload_delete($node) {
732
  $files = array();
733
  $result = db_query('SELECT * FROM {files} WHERE nid = %d', $node->nid);
734
735
736
737
738
  while ($file = db_fetch_object($result)) {
    $files[$file->fid] = $file;
  }

  foreach ($files as $fid => $file) {
739
    // Delete all file revision information associated with the node
740
    db_query('DELETE FROM {file_revisions} WHERE fid = %d', $fid);
Dries's avatar
 
Dries committed
741
742
    file_delete($file->filepath);
  }
743

744
  // Delete all files associated with the node
745
  db_query('DELETE FROM {files} WHERE nid = %d', $node->nid);
746
747
748
}

function upload_delete_revision($node) {
749
750
751
752
753
754
755
756
757
758
  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 {file_revisions} 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);
      }
759
760
761
762
763
    }
  }

  // delete the revision
  db_query('DELETE FROM {file_revisions} WHERE vid = %d', $node->vid);
Dries's avatar
 
Dries committed
764
765
}

766
function _upload_form($node) {
Gerhard Killesreiter's avatar
Gerhard Killesreiter committed
767

768
  $form['#theme'] = 'upload_form_new';
769

770
  if (is_array($node->files) && count($node->files)) {
771
772
    $form['files']['#theme'] = 'upload_form_current';
    $form['files']['#tree'] = TRUE;
Dries's avatar
 
Dries committed
773
    foreach ($node->files as $key => $file) {
774
      $description = file_create_url((strpos($file->fid, 'upload') === FALSE ? $file->filepath : file_create_filename($file->filename, file_create_path())));
Gerhard Killesreiter's avatar
Gerhard Killesreiter committed
775
      $description = "<small>". check_plain($description) ."</small>";
776
      $form['files'][$key]['description'] = array('#type' => 'textfield', '#default_value' => (strlen($file->description)) ? $file->description : $file->filename, '#maxlength' => 256, '#description' => $description );
Gerhard Killesreiter's avatar
Gerhard Killesreiter committed
777

778
      $form['files'][$key]['size'] = array('#value' => format_size($file->filesize));
779
780
      $form['files'][$key]['remove'] = array('#type' => 'checkbox', '#default_value' => $file->remove);
      $form['files'][$key]['list'] = array('#type' => 'checkbox',  '#default_value' => $file->list);
781
782
783
784
785
      // if the file was uploaded this page request, set value. this fixes the problem
      // formapi has recognizing new checkboxes. see comments in _upload_prepare.
      if ($_SESSION['file_current_upload'] == $file->fid) {
        $form['files'][$key]['list']['#value'] = variable_get('upload_list_default',1);
      }
786
787
788
789
790
      $form['files'][$key]['filename'] = array('#type' => 'value',  '#value' => $file->filename);
      $form['files'][$key]['filepath'] = array('#type' => 'value',  '#value' => $file->filepath);
      $form['files'][$key]['filemime'] = array('#type' => 'value',  '#value' => $file->filemime);
      $form['files'][$key]['filesize'] = array('#type' => 'value',  '#value' => $file->filesize);
      $form['files'][$key]['fid'] = array('#type' => 'value',  '#value' => $file->fid);
Dries's avatar
 
Dries committed
791
792
    }
  }
793

794
  if (user_access('upload files')) {
795
796
797
798
799
    // This div is hidden when the user uploads through JS.
    $form['new'] = array(
      '#prefix' => '<div id="attach-hide">',
      '#suffix' => '</div>',
    );
800
    $form['new']['upload'] = array('#type' => 'file', '#title' => t('Attach new file'), '#size' => 40);
801
    $form['new']['attach'] = array('#type' => 'button', '#value' => t('Attach'), '#name' => 'attach', '#attributes' => array('id' => 'attach-button'));
802
    // The class triggers the js upload behaviour.
803
    $form['attach-url'] = array('#type' => 'hidden', '#value' => url('upload/js', NULL, NULL, TRUE), '#attributes' => array('class' => 'upload'));
804
  }
Dries's avatar
 
Dries committed
805

806
807
  // Needed for JS
  $form['current']['vid'] = array('#type' => 'hidden', '#value' => $node->vid);
808
809
810
  return $form;
}

811
812
813
/**
 * Theme the attachments list.
 */
814
815
function theme_upload_form_current(&$form) {
  $header = array(t('Delete'), t('List'), t('Description'), t('Size'));
816
817

  foreach (element_children($form) as $key) {
818
    $row = array();
819
820
821
822
    $row[] = drupal_render($form[$key]['remove']);
    $row[] = drupal_render($form[$key]['list']);
    $row[] = drupal_render($form[$key]['description']);
    $row[] = drupal_render($form[$key]['size']);
823
824
825
    $rows[] = $row;
  }
  $output = theme('table', $header, $rows);
826
  $output .= drupal_render($form);
827
  return $output;
Dries's avatar
 
Dries committed
828
829
}

830
831
832
833
834
/**
 * Theme the attachment form.
 * Note: required to output prefix/suffix.
 */
function theme_upload_form_new($form) {
835
  $output = drupal_render($form);
836
837
838
  return $output;
}

Dries's avatar
 
Dries committed
839
840
841
function upload_load($node) {
  $files = array();

842
  if ($node->vid) {
843
    $result = db_query('SELECT * FROM {files} f INNER JOIN {file_revisions} r ON f.fid = r.fid WHERE r.vid = %d ORDER BY f.fid', $node->vid);
Dries's avatar
 
Dries committed
844
845
846
847
848
849
850
851
    while ($file = db_fetch_object($result)) {
      $files[$file->fid] = $file;
    }
  }

  return $files;
}

852
853
854
855
856
857
858
859
860
861
862
863
864
/**
 * Check an upload, if it is an image, make sure it fits within the
 * maximum dimensions allowed.
 */
function _upload_image($file) {
  $info = image_get_info($file->filepath);

  if ($info) {
    list($width, $height) = explode('x', variable_get('upload_max_resolution', 0));
    if ($width && $height) {
      $result = image_scale($file->filepath, $file->filepath, $width, $height);
      if ($result) {
        $file->filesize = filesize($file->filepath);
865
        drupal_set_message(t('The image was resized to fit within the maximum allowed resolution of %resolution pixels.', array('%resolution' => variable_get('upload_max_resolution', 0))));
866
867
868
869
870
871
872
      }
    }
  }

  return $file;
}

873
874
875
876
877
/**
 * Menu-callback for JavaScript-based uploads.
 */
function upload_js() {
  // We only do the upload.module part of the node validation process.
878
  $node = (object)$_POST;
879
880
881
882
883
884

  // Load existing node files.
  $node->files = upload_load($node);

  // Handle new uploads, and merge tmp files into node-files.
  _upload_prepare($node);
885
  _upload_validate($node);
886

887
  $form = _upload_form($node);
888
889
890
891
  foreach (module_implements('form_alter') as $module) {
    $function = $module .'_form_alter';
    $function('upload_js', $form);
  }
892
  $form = form_builder('upload_js', $form);
893
  $output = theme('status_messages') . drupal_render($form);
894
  // We send the updated file attachments form.
895
  print drupal_to_js(array('status' => TRUE, 'data' => $output));
896
897
  exit;
}