diff --git a/include/class.thread.php b/include/class.thread.php index 07073b11cacd338c4379599f862abc09db661ec8..42b18ec98840c8b9cce7323d1d0c7eb12a99dccb 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -881,98 +881,73 @@ implements TemplateVariable { return $this->hasFlag(self::FLAG_SYSTEM); } - //Web uploads - caller is expected to format, validate and set any errors. - function uploadFiles($files) { - - if(!$files || !is_array($files)) - return false; + protected function normalizeFileInfo($files, $add_error=true) { + static $error_descriptions = array( + UPLOAD_ERR_INI_SIZE => /* @trans */ 'File is too large', + UPLOAD_ERR_FORM_SIZE => /* @trans */ 'File is too large', + UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially uploaded.', + UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder.', + UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.', + UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the file upload.', + ); - $uploaded=array(); - foreach($files as $file) { - if($file['error'] && $file['error']==UPLOAD_ERR_NO_FILE) + if (!is_array($files)) + $files = array($files); + + $ids = array(); + foreach ($files as $name => $file) { + $F = array('inline' => is_array($file) && @$file['inline']); + + if (is_numeric($file)) + $fileId = $file; + elseif ($file instanceof AttachmentFile) + $fileId = $file->getId(); + elseif (is_array($file) && isset($file['id'])) + $fileId = $file['id']; + elseif ($AF = AttachmentFile::create($file)) + $fileId = $AF->getId(); + elseif ($add_error) { + $error = $file['error'] + ?: sprintf(_S('Unable to import attachment - %s'), + $name ?: $file['name']); + if (is_numeric($error) && isset($error_descriptions[$error])) { + $error = sprintf(_S('Error #%1$d: %2$s'), $error, + _S($error_descriptions[$error])); + } + // No need to log the missing-file error number + if ($error != UPLOAD_ERR_NO_FILE) + $this->getThread()->getObject()->logNote( + _S('File Import Error'), $error, _S('SYSTEM'), false); continue; - - if(!$file['error'] - && ($F=AttachmentFile::upload($file)) - && $this->saveAttachment($F)) - $uploaded[]= $F->getId(); - else { - if(!$file['error']) - $error = sprintf(__('Unable to upload file - %s'),$file['name']); - elseif(is_numeric($file['error'])) - $error ='Error #'.$file['error']; //TODO: Transplate to string. - else - $error = $file['error']; - /* - Log the error as an internal note. - XXX: We're doing it here because it will eventually become a thread post comment (hint: comments coming!) - XXX: logNote must watch for possible loops - */ - $this->getThread()->getObject()->logNote(__('File Upload Error'), $error, 'SYSTEM', false); } - } - - return $uploaded; - } - - function importAttachments(&$attachments) { + $F['id'] = $fileId; - if(!$attachments || !is_array($attachments)) - return null; - - $files = array(); - foreach($attachments as &$attachment) - if(($id=$this->importAttachment($attachment))) - $files[] = $id; - - return $files; - } - - /* Emailed & API attachments handler */ - function importAttachment(&$attachment) { + if (is_string($name)) + $F['name'] = $name; + if (isset($AF)) + $F['file'] = $AF; - if(!$attachment || !is_array($attachment)) - return null; + // Add things like the `key` field, but don't change current + // keys of the file array + if (is_array($file)) + $F += $file; - $A=null; - if ($attachment['error'] || !($A=$this->saveAttachment($attachment))) { - $error = $attachment['error']; - if(!$error) - $error = sprintf(_S('Unable to import attachment - %s'), - $attachment['name']); - //FIXME: logComment here - $this->getThread()->getObject()->logNote( - _S('File Import Error'), $error, _S('SYSTEM'), false); + $ids[] = $F; } - - return $A; + return $ids; } /* Save attachment to the DB. @file is a mixed var - can be ID or file hashtable. */ - function saveAttachment(&$file, $name=false) { - - $inline = is_array($file) && @$file['inline']; - - if (is_numeric($file)) - $fileId = $file; - elseif ($file instanceof AttachmentFile) - $fileId = $file->getId(); - elseif ($F = AttachmentFile::create($file)) - $fileId = $F->getId(); - elseif (is_array($file) && isset($file['id'])) - $fileId = $file['id']; - else - return false; - + function createAttachment($file, $name=false) { $att = new Attachment(array( 'type' => 'H', 'object_id' => $this->getId(), - 'file_id' => $fileId, - 'inline' => $inline ? 1 : 0, + 'file_id' => $file['id'], + 'inline' => $file['inline'] ? 1 : 0, )); // Record varying file names in the attachment record @@ -984,7 +959,7 @@ implements TemplateVariable { } if ($filename) { // This should be a noop since the ORM caches on PK - $F = $F ?: AttachmentFile::lookup($fileId); + $F = @$file['file'] ?: AttachmentFile::lookup($file['id']); // XXX: This is not Unicode safe if ($F && 0 !== strcasecmp($F->name, $filename)) $att->name = $filename; @@ -995,13 +970,11 @@ implements TemplateVariable { return $att; } - function saveAttachments($files) { - $attachments = array(); - foreach ($files as $name=>$file) { - if (($A = $this->saveAttachment($file, $name))) + function createAttachments(array $files) { + foreach ($files as $info) { + if ($A = $this->createAttachment($info, @$info['name'] ?: false)) $attachments[] = $A; } - return $attachments; } @@ -1326,29 +1299,6 @@ implements TemplateVariable { $vars['body'] = new TextThreadEntryBody($vars['body']); } - // Drop stripped images - if ($vars['attachments']) { - foreach ($vars['body']->getStrippedImages() as $cid) { - foreach ($vars['attachments'] as $i=>$a) { - if (@$a['cid'] && $a['cid'] == $cid) { - // Inline referenced attachment was stripped - unset($vars['attachments'][$i]); - } - } - } - } - - // Handle extracted embedded images (<img src="data:base64,..." />). - // The extraction has already been performed in the ThreadEntryBody - // class. Here they should simply be added to the attachments list - if ($atts = $vars['body']->getEmbeddedHtmlImages()) { - if (!is_array($vars['attachments'])) - $vars['attachments'] = array(); - foreach ($atts as $info) { - $vars['attachments'][] = $info; - } - } - if (!($body = $vars['body']->getClean())) $body = '-'; //Special tag used to signify empty message as stored. @@ -1377,11 +1327,6 @@ implements TemplateVariable { if (!($vars['staffId'] || $vars['userId'])) $entry->flags |= self::FLAG_SYSTEM; - if (!isset($vars['attachments']) || !$vars['attachments']) - // Otherwise, body will be configured in a block below (after - // inline attachments are saved and updated in the database) - $entry->body = $body; - if (isset($vars['pid'])) $entry->pid = $vars['pid']; // Check if 'reply_to' is in the $vars as the previous ThreadEntry @@ -1394,51 +1339,79 @@ implements TemplateVariable { if ($vars['ip_address']) $entry->ip_address = $vars['ip_address']; - if (!$entry->save()) - return false; - /************* ATTACHMENTS *****************/ + // Drop stripped email inline images + if ($vars['attachments']) { + foreach ($vars['body']->getStrippedImages() as $cid) { + foreach ($vars['attachments'] as $i=>$a) { + if (@$a['cid'] && $a['cid'] == $cid) { + // Inline referenced attachment was stripped + unset($vars['attachments'][$i]); + } + } + } + } - //Upload/save attachments IF ANY - if($vars['files']) //expects well formatted and VALIDATED files array. - $entry->uploadFiles($vars['files']); - - //Canned attachments... - if($vars['cannedattachments'] && is_array($vars['cannedattachments'])) - $entry->saveAttachments($vars['cannedattachments']); - - //Emailed or API attachments - if (isset($vars['attachments']) && $vars['attachments']) { - foreach ($vars['attachments'] as &$a) - if (isset($a['cid']) && $a['cid'] - && strpos($body, 'cid:'.$a['cid']) !== false) - $a['inline'] = true; - unset($a); - - $entry->importAttachments($vars['attachments']); - foreach ($vars['attachments'] as $a) { - // Change <img src="cid:"> inside the message to point to - // a unique hash-code for the attachment. Since the - // content-id will be discarded, only the unique hash-code - // will be available to retrieve the image later - if ($a['cid'] && $a['key']) { - $body = preg_replace('/src=("|\'|\b)(?:cid:)?' - . preg_quote($a['cid'], '/').'\1/i', - 'src="cid:'.$a['key'].'"', $body); + // Handle extracted embedded images (<img src="data:base64,..." />). + // The extraction has already been performed in the ThreadEntryBody + // class. Here they should simply be added to the attachments list + if ($atts = $vars['body']->getEmbeddedHtmlImages()) { + if (!is_array($vars['attachments'])) + $vars['attachments'] = array(); + foreach ($atts as $info) { + $vars['attachments'][] = $info; + } + } + + $attached_files = array(); + foreach (array( + // Web uploads and canned attachments + $vars['files'], $vars['cannedattachments'], + // Emailed or API attachments + $vars['attachments'], + // Inline images (attached to the draft) + Draft::getAttachmentIds($body), + ) as $files + ) { + if (is_array($files)) { + // Detect *inline* email attachments + foreach ($files as $i=>$a) { + if (isset($a['cid']) && $a['cid'] + && strpos($body, 'cid:'.$a['cid']) !== false) + $files[$i]['inline'] = true; + } + foreach ($entry->normalizeFileInfo($files) as $F) { + // Deduplicate on the `key` attribute + $attached_files[$F['key']] = $F; } } + } - $entry->body = $body; - if (!$entry->save()) - return false; + // Change <img src="cid:"> inside the message to point to a unique + // hash-code for the attachment. Since the content-id will be + // discarded, only the unique hash-code (key) will be available to + // retrieve the image later + foreach ($attached_files as $key => $a) { + if (isset($a['cid']) && $a['cid']) { + $body = preg_replace('/src=("|\'|\b)(?:cid:)?' + . preg_quote($a['cid'], '/').'\1/i', + 'src="cid:'.$key.'"', $body); + } } + // Set body here after it was rewritten to capture the stored file + // keys (above) + $entry->body = $body; + + if (!$entry->save()) + return false; + + // Associate the attached files with this new entry + $entry->createAttachments($attached_files); + // Save mail message id, if available $entry->saveEmailInfo($vars); - // Inline images (attached to the draft) - $entry->saveAttachments(Draft::getAttachmentIds($body)); - Signal::send('threadentry.created', $entry); return $entry;