? privatemsg_attachments/icon_attachment.gif
Index: privatemsg_attachments/privatemsg_attachments.info
===================================================================
RCS file: privatemsg_attachments/privatemsg_attachments.info
diff -N privatemsg_attachments/privatemsg_attachments.info
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ privatemsg_attachments/privatemsg_attachments.info	1 Feb 2010 08:58:19 -0000
@@ -0,0 +1,9 @@
+; $Id$
+name = Privatemsg attachments
+description = Allow users to add attachments to messages
+package = Mail
+core = 6.x
+dependencies[] = privatemsg
+dependencies[] = upload
+
+
Index: privatemsg_attachments/privatemsg_attachments.install
===================================================================
RCS file: privatemsg_attachments/privatemsg_attachments.install
diff -N privatemsg_attachments/privatemsg_attachments.install
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ privatemsg_attachments/privatemsg_attachments.install	1 Feb 2010 08:58:19 -0000
@@ -0,0 +1,78 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Installation hooks for privatemsg_attachments
+ */
+
+/**
+ * Implements hook_install().
+ */
+function privatemsg_attachments_install() {
+  drupal_install_schema('privatemsg_attachments');
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function privatemsg_attachments_uninstall() {
+  // Remove tables.
+  drupal_uninstall_schema('privatemsg_attachments');
+  variable_del('privatemsg_attachments_upload_dir');
+}
+
+/**
+ * Implements hook_schema().
+ */
+function privatemsg_attachments_schema() {
+  $schema['pm_attachments'] = array(
+    'description' => t('Stores uploaded file information and table associations for private messages.'),
+    'fields' => array(
+      'fid' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+        'description' => t('Primary Key: The {files}.fid.'),
+      ),
+      'mid' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+        'description' => t('The {pm_message}.mid associated with the uploaded file.'),
+      ),
+      'description' => array(
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+        'description' => t('Description of the uploaded file.'),
+      ),
+      'list' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+        'size' => 'tiny',
+        'description' => t('Whether the file should be visibly listed on the node: yes(1) or no(0).'),
+      ),
+      'weight' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+        'size' => 'tiny',
+        'description' => t('Weight of this upload in relation to other uploads in this node.'),
+      ),
+    ),
+    'primary key' => array('mid', 'fid'),
+    'indexes' => array(
+      'fid' => array('fid'),
+    ),
+  );
+
+  return $schema;
+}
+
+
Index: privatemsg_attachments/privatemsg_attachments.module
===================================================================
RCS file: privatemsg_attachments/privatemsg_attachments.module
diff -N privatemsg_attachments/privatemsg_attachments.module
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ privatemsg_attachments/privatemsg_attachments.module	1 Feb 2010 08:58:19 -0000
@@ -0,0 +1,397 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Allows users to add attachments to messages
+ */
+
+/**
+ * Implements hook_perm()
+ */
+function privatemsg_attachments_perm() {
+  return array('upload private message attachments', 'view private message attachments');
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function privatemsg_attachments_form_privatemsg_new_alter(&$form, &$form_state) {
+  if (user_access('upload private message attachments')) {
+    $form['privatemsg']['attachments'] = array(
+        '#type' => 'fieldset',
+        '#access' => user_access('upload private message attachments'),
+        '#title' => t('File attachments'),
+        '#collapsible' => TRUE,
+        '#collapsed' => empty($form['#privatemsg_message']['files']),
+        '#description' => t('Changes made to the attachments are not permanent until you send this message.'),
+        '#prefix' => '<div class="attachments">',
+        '#suffix' => '</div>',
+        '#weight' => 1,
+    );
+
+    // Wrapper for fieldset contents (used by ahah.js).
+    $form['privatemsg']['attachments']['wrapper'] = array(
+        '#prefix' => '<div id="attach-wrapper">',
+        '#suffix' => '</div>',
+    );
+
+    // Make sure necessary directories for upload.module exist and are
+    // writable before displaying the attachment form.
+    $path = file_directory_path();
+    $temp = file_directory_temp();
+    $dir = variable_get('privatemsg_attachments_upload_dir', '');
+    if (!empty($dir) && $dir[0] !== '/') {
+      $dir = '/' . $dir;
+    }
+    $path .= $dir;
+
+    // Note: pass by reference
+    if (!file_check_directory($path, FILE_CREATE_DIRECTORY) || !file_check_directory($temp, FILE_CREATE_DIRECTORY)) {
+      $form['privatemsg']['attachments']['#description'] =  t('File attachments are disabled. The file directories have not been properly configured.');
+      if (user_access('administer site configuration')) {
+        $form['privatemsg']['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['privatemsg']['attachments']['#description'] .= ' '. t('Please contact the site administrator.');
+      }
+    }
+    else {
+      $files = array();
+      if (!empty($form_state['values']['files'])) {
+        $files = $form_state['values']['files'];
+        $form['privatemsg']['attachments']['#collapsed'] = FALSE;
+      }
+
+      $form['privatemsg']['attachments']['wrapper'] += _privatemsg_attachments_form($files);
+      // Execute submit function as validate, to have it executed before
+      // $form_state['validate_built_message'] is created.
+      array_unshift($form['#validate'], '_privatemsg_attachments_upload_submit');
+      $form['#attributes']['enctype'] = 'multipart/form-data';
+    }
+  }
+}
+
+function privatemsg_attachments_form_privatemsg_admin_settings_alter(&$form, &$form_state) {
+  $form['attachments'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Privatemsg attachments'),
+    '#collapsible' => TRUE,
+    '#collapsed' => TRUE,
+    '#weight' => -1,
+  );
+
+  $form['attachments']['privatemsg_attachments_upload_dir'] = array(
+    '#title' => t('Privatemsg attachments directory'),
+    '#type'  => 'textfield',
+    '#default_value' => variable_get('privatemsg_attachments_upload_dir', ''),
+    '#description' => t('Relative path, based on the file system path, where private message attachments will be stored.'),
+    '#weight' => -4,
+  );
+
+  // Add tags to the list of possible columns.
+  $form['privatemsg_listing']['privatemsg_display_fields']['#options']['attachment'] = t('Attachment icon');
+}
+
+function _privatemsg_attachments_form($files = array()) {
+  global $user;
+
+  $form = array(
+    '#theme' => 'upload_form_new',
+    '#cache' => TRUE,
+  );
+
+  if (!empty($files)) {
+    $form['files']['#theme'] = 'upload_form_current';
+    $form['files']['#tree'] = TRUE;
+    foreach ($files as $key => $file) {
+      $file = (object)$file;
+      $description = file_create_url($file->filepath);
+      $description = "<small>". check_plain($description) ."</small>";
+      $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]['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($files), '#default_value' => $file->weight);
+      $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);
+      $form['files'][$key]['new'] = array('#type' => 'value', '#value' => $file->new);
+    }
+  }
+
+  $limits = _upload_file_limits($user);
+  $form['new']['#weight'] = 10;
+  $form['new']['upload'] = array(
+      '#type' => 'file',
+      '#title' => t('Attach new file'),
+      '#size' => 40,
+      '#description' => ($limits['resolution'] ? t('Images are larger than %resolution will be resized.', array('%resolution' => $limits['resolution'])) : '') .' '. t('The maximum upload size is %filesize. Only files with the following extensions may be uploaded: %extensions.', array('%extensions' => $limits['extensions'], '%filesize' => format_size($limits['file_size']))),
+  );
+  $form['new']['attach'] = array(
+      '#type' => 'submit',
+      '#value' => t('Attach'),
+      '#name' => 'attach',
+      '#ahah' => array(
+        'path' => 'messages/upload/js',
+        'wrapper' => 'attach-wrapper',
+        'progress' => array('type' => 'bar', 'message' => t('Please wait...')),
+    ),
+  );
+  return $form;
+}
+
+/**
+ * Implement hook_menu().
+ */
+function privatemsg_attachments_menu() {
+  $items['messages/upload/js'] = array(
+    'page callback' => 'privatemsg_attachments_upload_js',
+    'access arguments' => array('upload files'),
+    'type' => MENU_CALLBACK,
+  );
+  return $items;
+}
+
+/**
+ * Menu-callback for JavaScript-based uploads.
+ */
+function privatemsg_attachments_upload_js() {
+  $files = array();
+
+  $form_state = array('values' => $_POST, 'storage' => NULL, 'submitted' => FALSE);
+  // Load the form from the Form API cache.
+  if (!($cached_form = form_get_cache($_POST['form_build_id'], $form_state)) || !isset($cached_form['privatemsg']['attachments'])) {
+    form_set_error('form_token', t('Validation error, please try again. If this error persists, please contact the site administrator.'));
+    $output = theme('status_messages');
+
+    drupal_json(array('status' => TRUE, 'data' => $output));
+    exit();
+  }
+
+  // Handle new uploads, and merge tmp files into node-files.
+  _privatemsg_attachments_upload_submit($cached_form, $form_state);
+
+  if (!empty($form_state['values']['files'])) {
+    foreach ($form_state['values']['files'] as $fid => $file) {
+      $files[$fid] = $file;
+    }
+  }
+
+  $form = _privatemsg_attachments_form($files);
+
+  unset($cached_form['privatemsg']['attachments']['wrapper']['new']);
+  $cached_form['privatemsg']['attachments']['wrapper'] = array_merge($cached_form['privatemsg']['attachments']['wrapper'], $form);
+
+  $cached_form['privatemsg']['attachments']['#collapsed'] = FALSE;
+
+  form_set_cache($_POST['form_build_id'], $cached_form, $form_state);
+
+  foreach ($files as $fid => $file) {
+    if (is_numeric($fid)) {
+      $form['files'][$fid]['description']['#default_value'] = $form_state['values']['files'][$fid]['description'];
+      $form['files'][$fid]['list']['#default_value'] = !empty($form_state['values']['files'][$fid]['list']);
+      $form['files'][$fid]['remove']['#default_value'] = !empty($form_state['values']['files'][$fid]['remove']);
+      $form['files'][$fid]['weight']['#default_value'] = $form_state['values']['files'][$fid]['weight'];
+    }
+  }
+
+  // Render the form for output.
+  $form += array(
+    '#post' => $_POST,
+    '#programmed' => FALSE,
+    '#tree' => FALSE,
+    '#parents' => array(),
+    '#id' => '_privatemsg_upload_form',
+  );
+  drupal_alter('form', $form, array(), '_privatemsg_upload_form');
+  $form_state = array('submitted' => FALSE);
+  $form = form_builder('_privatemsg_upload_form', $form, $form_state);
+
+  $output = theme('status_messages') . drupal_render($form);
+
+  // We send the updated file attachments form.
+  // Don't call drupal_json(). ahah.js uses an iframe and
+  // the header output by drupal_json() causes problems in some browsers.
+  print drupal_to_js(array('status' => TRUE, 'data' => $output));
+  exit;
+}
+
+function _privatemsg_attachments_upload_submit(&$form, &$form_state) {
+  global $user;
+
+  $limits = _upload_file_limits($user);
+  $validators = array(
+    'file_validate_extensions' => array($limits['extensions']),
+    'file_validate_image_resolution' => array($limits['resolution']),
+    'file_validate_size' => array($limits['file_size'], $limits['user_size']),
+  );
+
+  // Save new file uploads.
+  $dir = variable_get('privatemsg_attachments_upload_dir', '');
+  if (!empty($dir) && $dir[0] !== '/') {
+    $dir = '/' . $dir;
+  }
+  if (user_access('upload private message attachments') && ($file = file_save_upload('upload', $validators, file_directory_path() . $dir))) {
+    $file->list = variable_get('upload_list_default', 1);
+    $file->description = $file->filename;
+    $file->weight = 0;
+    $file->new = TRUE;
+    $form_state['values']['files'][$file->fid] = (array)$file;
+  }
+
+  // Order the form according to the set file weight values.
+  if (!empty($form_state['values']['files'])) {
+    $microweight = 0.001;
+    foreach ($form_state['values']['files'] as $fid => $file) {
+      if (is_numeric($fid)) {
+        $form_state['values']['files'][$fid]['#weight'] = $file['weight'] + $microweight;
+        $microweight += 0.001;
+      }
+    }
+    uasort($form_state['values']['files'], 'element_sort');
+  }
+}
+
+function privatemsg_attachments_privatemsg_message_view_alter(&$vars) {
+  if (!empty($vars['message']['files']) && user_access('view private message attachments')) {
+    $vars['message_body'] .= theme('upload_attachments', $vars['message']['files']);
+  }
+}
+
+/**
+ * Implement hook_privatemsg_message_insert().
+ */
+function privatemsg_attachments_privatemsg_message_insert($message) {
+
+  if (empty($message['files']) || !is_array($message['files'])) {
+    return;
+  }
+
+  foreach ($message['files'] as $fid => $file) {
+    // Convert file to object for compatibility
+    $file = (object)$file;
+
+    // Remove file. Process removals first since no further processing
+    // will be required.
+    if (!empty($file->remove)) {
+      db_query('DELETE FROM {pm_attachments} WHERE fid = %d AND mid = %d', $fid, $message['mid']);
+
+      file_delete($file->filepath);
+      db_query('DELETE FROM {files} WHERE fid = %d', $fid);
+
+      // Remove it from the session in the case of new uploads,
+      // that you want to disassociate before node submission.
+      unset($message['files'][$fid]);
+      // Move on, so the removed file won't be added to new revisions.
+      continue;
+    }
+
+    // Create a new revision, or associate a new file needed.
+    if ($file->new) {
+      db_query("INSERT INTO {pm_attachments} (fid, mid, list, description, weight) VALUES (%d, %d, %d, '%s', %d)", $file->fid, $message['mid'], $file->list, $file->description, $file->weight);
+      file_set_status($file, FILE_STATUS_PERMANENT);
+    }
+  }
+}
+
+/**
+ * Implement hook_privatemsg_message_load().
+ */
+function privatemsg_attachments_privatemsg_message_load($message) {
+  $files = array();
+
+  $result = db_query('SELECT * FROM {files} f INNER JOIN {pm_attachments} pma ON f.fid = pma.fid WHERE pma.mid = %d ORDER BY pma.weight, f.fid', $message['mid']);
+  while ($file = db_fetch_object($result)) {
+    $files[$file->fid] = $file;
+  }
+
+  return array('files' => $files);
+}
+
+/**
+ * Implement hook_privatemsg_message_flush().
+ */
+function privatemsg_attachments_privatemsg_message_flush($message) {
+  if (!isset($message['files'])) {
+    return;
+  }
+
+  foreach ($message['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 {pm_attachments} WHERE mid = %d', $message['mid']);
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * Add attachment data to the table data.
+ */
+function privatemsg_attachments_form_privatemsg_list_alter(&$form, &$form_state) {
+  // Load enabled columns.
+  $fields = array_filter(variable_get('privatemsg_display_fields', array('participants')));
+  if (in_array('attachment', $fields) && !empty($form['#data'])) {
+    // Load thread id's of the current list.
+    $threads = array_keys($form['#data']);
+
+    // Fetch all tags of those threads.
+    $query = _privatemsg_assemble_query(array('count', 'privatemsg_attachments'), $threads);
+
+    // Add them to #data
+    $result = db_query($query['query']);
+    while ($attachment = db_fetch_array($result)) {
+      $form['#data'][$attachment['thread_id']]['attachment'] = $attachment['count'];
+    }
+  }
+}
+
+function privatemsg_attachments_sql_count(&$fragments, $threads) {
+  $fragments['primary_table'] = '{pm_attachments} pma';
+
+  $fragments['select'][] = 'pmi.thread_id';
+  $fragments['select'][] = 'COUNT(DISTINCT pma.fid) AS count';
+
+  $fragments['inner_join'][] = 'INNER JOIN {pm_index} pmi ON (pmi.mid = pma.mid)';
+
+  $fragments['where'][] = 'pmi.thread_id IN (' . db_placeholders($threads) . ')';
+  $fragments['query_args']['where'] = $threads;
+
+  $fragments['group_by'][] = 'pmi.thread_id';
+
+}
+
+/**
+ * Define the header for the attachment column.
+ *
+ * @see theme_privatemsg_list_header()
+ */
+function phptemplate_privatemsg_list_header__attachment() {
+   return array(
+    'data' => '&nbsp;',
+    'key' => 'attachment',
+    'class' => 'privatemsg-header-attachment',
+    '#weight' => 0,
+  );
+}
+
+
+/**
+ * Default theme pattern function to display tags.
+ *
+ * @see theme_privatemsg_list_field()
+ */
+function phptemplate_privatemsg_list_field__attachment($thread) {
+  if (isset($thread['attachment'])) {
+    $description = format_plural($thread['attachment'], t('Thread contains @count attachment.'), t('Thread contains @count attachments.'));
+    $img = theme('image', drupal_get_path('module', 'privatemsg_attachments') . '/icon_attachment.gif', $description, $description);
+    return $img;
+  }
+}
+
Index: privatemsg_attachments/privatemsg_attachments.test
===================================================================
RCS file: privatemsg_attachments/privatemsg_attachments.test
diff -N privatemsg_attachments/privatemsg_attachments.test
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ privatemsg_attachments/privatemsg_attachments.test	1 Feb 2010 08:58:19 -0000
@@ -0,0 +1,86 @@
+<?php
+// $Id: upload.test,v 1.30 2009/12/02 19:26:23 dries Exp $
+
+/**
+ * @file
+ * This file contains tests for the upload module.
+ */
+
+
+class PrivatemsgAttachmentsTestCase extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Privatemsg Attachments functionality',
+      'description' => 'Check content uploaded to private messages.',
+      'group' => 'Privatemsg',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('upload', 'privatemsg', 'privatemsg_attachments');
+  }
+
+  /**
+   * Create node; upload files to node; and edit, and delete uploads.
+   */
+  function testUpload() {
+    global $base_url;
+    $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer privatemsg settings'));
+    $author = $this->drupalCreateUser(array('access content', 'edit own page content', 'upload private message attachments', 'view private message attachments', 'write privatemsg', 'read privatemsg'));
+    $recipient = $this->drupalCreateUser(array('read privatemsg', 'view private message attachments'));
+
+    $this->drupalLogin($admin_user);
+
+    $edit = array();
+    $edit['privatemsg_attachments_upload_dir'] = $this->randomName(10);
+    $this->drupalPost('admin/settings/messages', $edit, t('Save configuration'));
+    $this->assertText('The configuration options have been saved.', 'Privatemsg setting saved.');
+
+    // Setup upload settings.
+    $edit = array();
+    $edit['upload_list_default'] = '1'; // Yes.
+    $edit['upload_extensions_default'] = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
+    $edit['upload_uploadsize_default'] = '1.5';
+    $edit['upload_usersize_default'] = '1.5';
+    $this->drupalPost('admin/settings/uploads', $edit, t('Save configuration'));
+    $this->assertText('The configuration options have been saved.', 'Upload setting saved.');
+
+    $this->drupalLogin($author);
+
+    // Create a node and attempt to attach files.
+    $text_files = $this->drupalGetTestFiles('text');
+    $files = array(current($text_files)->filename, next($text_files)->filename);
+
+    // Prepare edit arrays, single recipient.
+    $edit = array(
+      'recipient'     => $recipient->name,
+      'subject'       => $this->randomName(20),
+      'body'          => $this->randomName(100),
+      'files[upload]' => $files[0],
+    );
+
+    // Preview first.
+    $this->drupalPost('messages/new', $edit, t('Preview message'));
+
+    // Make sure the attachment is still displayed:
+    $this->assertText(basename($files[0]), basename($files[0]) . ' found on message.');
+
+    // Submit the message now.
+    $this->drupalPost('messages/new', $edit, t('Send message'));
+    $this->assertText(t('A message has been sent to @recipients.', array('@recipients' => $recipient->name)), 'Message sent confirmation displayed.');
+
+    $this->drupalGet('messages');
+    $this->clickLink($edit['subject']);
+    // Check to see that uploaded file is listed in detail page and actually accessible.
+    $this->assertText(basename($files[0]), basename($files[0]) . ' found on message.');
+
+    // Prepare edit arrays, single recipient.
+    $reply = array(
+      'body'          => $this->randomName(100),
+      'files[upload]' => $files[1],
+    );
+
+    $this->drupalPost(NULL, $reply, t('Send message'));
+    $this->assertText(basename($files[1]), basename($files[1]) . ' found on message.');
+  }
+}
