Skip to content
Snippets Groups Projects
class.thread.php 85.2 KiB
Newer Older
<?php
/*********************************************************************
    class.thread.php

Peter Rotich's avatar
Peter Rotich committed
    Thread of things!
Peter Rotich's avatar
Peter Rotich committed
    XXX: Please DO NOT add any ticket related logic! use ticket class.

    Peter Rotich <peter@osticket.com>
    Copyright (c)  2006-2013 osTicket
    http://www.osticket.com

    Released under the GNU General Public License WITHOUT ANY WARRANTY.
    See LICENSE.TXT for details.

    vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/
include_once(INCLUDE_DIR.'class.ticket.php');
include_once(INCLUDE_DIR.'class.draft.php');
include_once(INCLUDE_DIR.'class.role.php');
Peter Rotich's avatar
Peter Rotich committed
//Ticket thread.
class Thread extends VerySimpleModel {
    static $meta = array(
        'table' => THREAD_TABLE,
        'pk' => array('id'),
        'joins' => array(
            'ticket' => array(
                'constraint' => array(
                    'object_type' => "'T'",
                    'object_id' => 'TicketModel.ticket_id',
            'task' => array(
                'constraint' => array(
                    'object_type' => "'A'",
                    'object_id' => 'Task.id',
                ),
            ),
            'collaborators' => array(
                'reverse' => 'Collaborator.thread',
            ),
                'reverse' => 'ThreadEntry.thread',
            'events' => array(
                'reverse' => 'ThreadEvent.thread',
    const MODE_STAFF = 1;
    const MODE_CLIENT = 2;
    var $_entries;
    var $_collaborators; // Cache for collabs
    var $_participants;
Peter Rotich's avatar
Peter Rotich committed

    function getId() {
Peter Rotich's avatar
Peter Rotich committed
    function getObjectId() {
Peter Rotich's avatar
Peter Rotich committed
    function getObjectType() {
    function getObject() {
        if (!$this->_object)
            $this->_object = ObjectModel::lookup(
                    $this->getObjectId(), $this->getObjectType());
        return $this->_object;
    function getNumAttachments() {
        return Attachment::objects()->filter(array(
            'thread_entry__thread' => $this
        ))->count();
Peter Rotich's avatar
Peter Rotich committed
    function getNumEntries() {
        return $this->entries->count();
    }
    function getEntries($criteria=false) {
        if (!isset($this->_entries)) {
            $this->_entries = $this->entries->annotate(array(
                'has_attachments' => SqlAggregate::COUNT(SqlCase::N()
                    ->when(array('attachments__inline'=>0), 1)
                    ->otherwise(null)
                ),
            ));
            $this->_entries->exclude(array('flags__hasbit'=>ThreadEntry::FLAG_HIDDEN));
            if ($criteria)
                $this->_entries->filter($criteria);
        }
        return $this->_entries;
    // Collaborators
    function getNumCollaborators() {
        return $this->collaborators->count();
    function getNumActiveCollaborators() {
        if (!isset($this->ht['active_collaborators']))
            $this->ht['active_collaborators'] = count($this->getActiveCollaborators());
        return $this->ht['active_collaborators'];
    function getActiveCollaborators() {
        return $this->getCollaborators(array('isactive'=>1));
    function getCollaborators($criteria=array()) {
        if ($this->_collaborators && !$criteria)
            return $this->_collaborators;
        $collaborators = $this->collaborators
            ->filter(array('thread_id' => $this->getId()));

        if (isset($criteria['isactive']))
            $collaborators->filter(array('isactive' => $criteria['isactive']));

        // TODO: sort by name of the user
        $collaborators->order_by('user__name');

        if (!$criteria)
            $this->_collaborators = $collaborators;

        return $collaborators;
    function addCollaborator($user, $vars, &$errors, $event=true) {

        if (!$user)
            return null;

        $vars = array_merge(array(
                'threadId' => $this->getId(),
                'userId' => $user->getId()), $vars);
        if (!($c=Collaborator::add($vars, $errors)))
            return null;

        $this->_collaborators = null;

        if ($event)
            $this->getEvents()->log($this->getObject(),
                'collab',
                array('add' => array($user->getId() => array(
                        'name' => $user->getName()->getOriginal(),
                        'src' => @$vars['source'],
                    ))
                )
            );

        return $c;
    function updateCollaborators($vars, &$errors) {
        global $thisstaff;

        if (!$thisstaff) return;

        //Deletes
        if($vars['del'] && ($ids=array_filter($vars['del']))) {
            $collabs = array();
            foreach ($ids as $k => $cid) {
                if (($c=Collaborator::lookup($cid))
                        && $c->getThreadId() == $this->getId()
                        && $c->delete())
                     $collabs[] = $c;
            $this->getEvents()->log($this->getObject(), 'collab', array(
                'del' => array($c->user_id => array('name' => $c->getName()->getOriginal()))
            ));
        //statuses
        $cids = null;
        if($vars['cid'] && ($cids=array_filter($vars['cid']))) {
            $this->collaborators->filter(array(
                'thread_id' => $this->getId(),
                'id__in' => $cids
            ))->update(array(
                'updated' => SqlFunction::NOW(),
                'isactive' => 1,
            ));
        }
        $this->collaborators->filter(array(
            'thread_id' => $this->getId(),
            Q::not(array('id__in' => $cids ?: array(0)))
        ))->update(array(
            'updated' => SqlFunction::NOW(),
            'isactive' => 0,
        ));

        unset($this->ht['active_collaborators']);
        $this->_collaborators = null;

        return true;
    //UserList of participants (collaborators)
    function getParticipants() {

        if (!isset($this->_participants)) {
            $list = new UserList();
            if ($collabs = $this->getActiveCollaborators()) {
                foreach ($collabs as $c)
                    $list->add($c);
            }

            $this->_participants = $list;
        }
        return $this->_participants;
    // Render thread
    function render($type=false, $options=array()) {
        $mode = $options['mode'] ?: self::MODE_STAFF;
        // Register thread actions prior to rendering the thread.
        if (!class_exists('tea_showemailheaders'))
            include_once INCLUDE_DIR . 'class.thread_actions.php';
        $entries = $this->getEntries();
        if ($type && is_array($type))
            $entries->filter(array('type__in' => $type));
Peter Rotich's avatar
Peter Rotich committed
        if ($options['sort'] && !strcasecmp($options['sort'], 'DESC'))
            $entries->order_by('-id');
        // Precache all the attachments on this thread
        AttachmentFile::objects()->filter(array(
            'attachments__thread_entry__thread__id' => $this->id
        ))->all();
        $events = $this->getEvents();
        $inc = ($mode == self::MODE_STAFF) ? STAFFINC_DIR : CLIENTINC_DIR;
        include $inc . 'templates/thread-entries.tmpl.php';
Peter Rotich's avatar
Peter Rotich committed

    function getEntry($id) {
Peter Rotich's avatar
Peter Rotich committed
        return ThreadEntry::lookup($id, $this->getId());
    function getEvents() {
        return $this->events;
    }
    /**
     * postEmail
     *
     * After some security and sanity checks, attaches the body and subject
     * of the message in reply to this thread item
     *
     * Parameters:
     * mailinfo - (array) of information about the email, with at least the
     *          following keys
     *      - mid - (string) email message-id
     *      - name - (string) personal name of email originator
     *      - email - (string<email>) originating email address
     *      - subject - (string) email subject line (decoded)
     *      - body - (string) email message body (decoded)
     */
    function postEmail($mailinfo, $entry=null) {
        // +==================+===================+=============+
        // | Orig Thread-Type | Reply Thread-Type | Requires    |
        // +==================+===================+=============+
        // | *                | Message (M)       | From: Owner |
        // | *                | Note (N)          | From: Staff |
        // | Response (R)     | Message (M)       |             |
        // | Message (M)      | Response (R)      | From: Staff |
        // +------------------+-------------------+-------------+
        if (!$object = $this->getObject()) {
            // How should someone find this thread?
Peter Rotich's avatar
Peter Rotich committed
            return false;
        }
        elseif ($object instanceof Ticket && (
               !$mailinfo['staffId']
            && $object->isClosed()
            && !$object->isReopenable()
        )) {
            // Ticket is closed, not reopenable, and email was not submitted
            // by an agent. Email cannot be submitted
            return false;
        }
        $vars = array(
            'mid' =>    $mailinfo['mid'],
            'header' => $mailinfo['header'],
            'poster' => $mailinfo['name'],
            'origin' => 'Email',
            'source' => 'Email',
            'ip' =>     '',
            'reply_to' => $entry,
            'recipients' => $mailinfo['recipients'],
            'to-email-id' => $mailinfo['to-email-id'],
        );
        // XXX: Is this necessary?
        if ($object instanceof Ticket)
            $vars['ticketId'] = $object->getId();
        if ($object instanceof Task)
            $vars['taskId'] = $object->getId();
        $errors = array();
        if (isset($mailinfo['attachments']))
            $vars['attachments'] = $mailinfo['attachments'];
        $body = $mailinfo['message'];
        // Attempt to determine the user posting the entry and the
        // corresponding entry type by the information determined by the
        // mail parser (via the In-Reply-To header)
        switch ($mailinfo['userClass']) {
        case 'C': # Thread collaborator
            $vars['flags'] = ThreadEntry::FLAG_COLLABORATOR;
        case 'U': # Ticket owner
            $vars['thread-type'] = 'M';
            $vars['userId'] = $mailinfo['userId'];
            break;
        case 'A': # System administrator
        case 'S': # Staff member (agent)
            $vars['thread-type'] = 'N';
            $vars['staffId'] = $mailinfo['staffId'];
            if ($vars['staffId'])
                $vars['poster'] = Staff::lookup($mailinfo['staffId']);

        // The user type was not identified by the mail parsing system. It
        // is likely that the In-Reply-To and References headers were not
        // properly brokered by the user's mail client. Use the old logic to
        // determine the post type.
        default:
            // Disambiguate if the user happens also to be a staff member of
            // the system. The current ticket owner should _always_ post
            // messages instead of notes or responses
            if ($object instanceof Ticket
                && strcasecmp($mailinfo['email'], $object->getEmail()) == 0
            ) {
                $vars['thread-type'] = 'M';
                $vars['userId'] = $object->getUserId();
            }
            // Consider collaborator role (disambiguate staff members as
            // collaborators). Normally, the block above should match based
            // on the Referenced message-id header
            elseif ($C = $this->collaborators->filter(array(
                'user__emails__address' => $mailinfo['email']
            ))->first()) {
                $vars['thread-type'] = 'M';
                // XXX: There's no way that mailinfo[userId] would be set
                $vars['userId'] = $mailinfo['userId'] ?: $C->getUserId();
                $vars['flags'] = ThreadEntry::FLAG_COLLABORATOR;
            }
            // Don't process the email -- it came FROM this system
            elseif (Email::getIdByEmail($mailinfo['email'])) {
                return false;
Peter Rotich's avatar
Peter Rotich committed
        // Ensure we record the name of the person posting
        $vars['poster'] = $vars['poster']
            ?: $mailinfo['name'] ?: $mailinfo['email'];

        // TODO: Consider security constraints
        if (!$vars['thread-type']) {
            //XXX: Are we potentially leaking the email address to
            // collaborators?
            // Try not to destroy the format of the body
            $header = sprintf(
                _S('Received From: %1$s <%2$s>') . "\n\n",
                $mailinfo['name'], $mailinfo['email']);
            if ($body instanceof HtmlThreadEntryBody)
                $header = nl2br(Format::htmlchars($header));
            // Add the banner to the top of the message
            if ($body instanceof ThreadEntryBody)
                $body->prepend($header);
            $vars['userId'] = 0; //Unknown user! //XXX: Assume ticket owner?
            $vars['thread-type'] = 'M';
        }

        switch ($vars['thread-type']) {
        case 'M':
            $vars['message'] = $body;

            if ($object instanceof Threadable)
                return $object->postThreadEntry('M', $vars);
            elseif ($this instanceof ObjectThread)
                return $this->addMessage($vars, $errors);
            break;

        case 'N':
            $vars['note'] = $body;

            if ($object instanceof Threadable)
                return $object->postThreadEntry('N', $vars);
            elseif ($this instanceof ObjectThread)
                return $this->addNote($vars, $errors);

        throw new Exception('Unable to continue thread via email.');

        // Currently impossible, but indicate that this thread object could
        // not append the incoming email.
        return false;
Peter Rotich's avatar
Peter Rotich committed

    function deleteAttachments() {
        $deleted = Attachment::objects()->filter(array(
            'thread_entry__thread' => $this,
        ))->delete();
Peter Rotich's avatar
Peter Rotich committed
            AttachmentFile::deleteOrphans();
Peter Rotich's avatar
Peter Rotich committed

        return $deleted;
    }

    function removeCollaborators() {
        return Collaborator::objects()
            ->filter(array('thread_id'=>$this->getId()))
            ->delete();
    /**
     * Function: lookupByEmailHeaders
     *
     * Attempt to locate a thread by the email headers. It should be
     * considered a secondary lookup to ThreadEntry::lookupByEmailHeaders(),
     * which should find an actual thread entry, which should be possible
     * for all email communcation which is associated with a thread entry.
     * The only time where this is useful is for threads which triggered
     * email communication without a thread entry, for instance, like
     * tickets created without an initial message.
     */
    function lookupByEmailHeaders(&$mailinfo) {
        $possibles = array();
        foreach (array('mid', 'in-reply-to', 'references') as $header) {
            $matches = array();
            if (!isset($mailinfo[$header]) || !$mailinfo[$header])
                continue;
            // Header may have multiple entries (usually separated by
            // spaces ( )
            elseif (!preg_match_all('/<([^>@]+@[^>]+)>/', $mailinfo[$header],
            // The References header will have the most recent message-id
            // (parent) on the far right.
            // @see rfc 1036, section 2.2.5
            // @see http://www.jwz.org/doc/threading.html
            $possibles = array_merge($possibles, array_reverse($matches[1]));
        // Add the message id if it is embedded in the body
        $match = array();
        if (preg_match('`(?:class="mid-|Ref-Mid: )([^"\s]*)(?:$|")`',
                $mailinfo['message'], $match)
            && !in_array($match[1], $possibles)
        ) {
            $possibles[] = $match[1];
        }
        foreach ($possibles as $mid) {
            // Attempt to detect the ticket and user ids from the
            // message-id header. If the message originated from
            // osTicket, the Mailer class can break it apart. If it came
            // from this help desk, the 'loopback' property will be set
            // to true.
            $mid_info = Mailer::decodeMessageId($mid);
            if (!$mid_info || !$mid_info['loopback'])
                continue;
            if (isset($mid_info['uid'])
                && @$mid_info['threadId']
                && ($t = Thread::lookup($mid_info['threadId']))
            ) {
                if (@$mid_info['userId']) {
                    $mailinfo['userId'] = $mid_info['userId'];
                }
                elseif (@$mid_info['staffId']) {
                    $mailinfo['staffId'] = $mid_info['staffId'];
                }
                // ThreadEntry was positively identified
                return $t;
            }
        }
Peter Rotich's avatar
Peter Rotich committed
    function delete() {
Peter Rotich's avatar
Peter Rotich committed
        //Self delete
Peter Rotich's avatar
Peter Rotich committed
        // Clear email meta data (header..etc)
        ThreadEntryEmailInfo::objects()
            ->filter(array('thread_entry__thread' => $this))
            ->update(array('headers' => null));
Peter Rotich's avatar
Peter Rotich committed

        // Mass delete entries
Peter Rotich's avatar
Peter Rotich committed
        $this->deleteAttachments();
        $this->removeCollaborators();
        // Null out the events
        $this->events->update(array('thread_id' => 0));

Peter Rotich's avatar
Peter Rotich committed
        return true;
    }

    static function create($vars=false) {
        $inst = new static($vars);
        $inst->created = SqlFunction::NOW();
        return $inst;
Peter Rotich's avatar
Peter Rotich committed
    }
Peter Rotich's avatar
Peter Rotich committed

class ThreadEntryEmailInfo extends VerySimpleModel {
    static $meta = array(
        'table' => THREAD_ENTRY_EMAIL_TABLE,
        'pk' => array('id'),
        'joins' => array(
            'thread_entry' => array(
                'constraint' => array('thread_entry_id' => 'ThreadEntry.id'),
            ),
        ),
    );
class ThreadEntry extends VerySimpleModel
implements TemplateVariable {
    static $meta = array(
        'table' => THREAD_ENTRY_TABLE,
        'pk' => array('id'),
        'select_related' => array('staff', 'user', 'email_info'),
        'ordering' => array('created', 'id'),
        'joins' => array(
            'thread' => array(
                'constraint' => array('thread_id' => 'Thread.id'),
            'parent' => array(
                'constraint' => array('pid' => 'ThreadEntry.id'),
                'null' => true,
            ),
            'children' => array(
                'reverse' => 'ThreadEntry.parent',
            ),
            'email_info' => array(
                'reverse' => 'ThreadEntryEmailInfo.thread_entry',
                'list' => false,
            ),
                'reverse' => 'Attachment.thread_entry',
                'null' => true,
            ),
            'staff' => array(
                'constraint' => array('staff_id' => 'Staff.staff_id'),
                'null' => true,
            ),
            'user' => array(
                'constraint' => array('user_id' => 'User.id'),
    const FLAG_ORIGINAL_MESSAGE         = 0x0001;
    const FLAG_EDITED                   = 0x0002;
    const FLAG_HIDDEN                   = 0x0004;
    const FLAG_GUARDED                  = 0x0008;   // No replace on edit
    const FLAG_RESENT                   = 0x0010;
    const FLAG_COLLABORATOR             = 0x0020;   // Message from collaborator
    const FLAG_BALANCED                 = 0x0040;   // HTML does not need to be balanced on ::display()
    const FLAG_SYSTEM                   = 0x0080;   // Entry is a system note.
    const PERM_EDIT     = 'thread.edit';

    var $is_autoreply;
    var $is_bounce;
    static protected $perms = array(
        self::PERM_EDIT => array(
            'title' => /* @trans */ 'Edit Thread',
            'desc'  => /* @trans */ 'Ability to edit thread items of other agents',
        ),
    );
    function postEmail($mailinfo) {
        global $ost;
        if (!($thread = $this->getThread()))
            // Kind of hard to continue a discussion without a thread ...
        elseif ($this->getEmailMessageId() == $mailinfo['mid'])
            // Reporting success so the email can be moved or deleted.
            return true;
        // Mail sent by this system will have a predictable message-id
        // If this incoming mail matches the code, then it very likely
        // originated from this system and looped
        $info = Mailer::decodeMessageId($mailinfo['mid']);
        if ($info && $info['loopback']) {
            // This mail was sent by this system. It was received due to
            // some kind of mail delivery loop. It should not be considered
            // a response to an existing thread entry
            if ($ost)
                $ost->log(LOG_ERR, _S('Email loop detected'), sprintf(
                _S('It appears as though &lt;%s&gt; is being used as a forwarded or fetched email account and is also being used as a user / system account. Please correct the loop or seek technical assistance.'),
                $mailinfo['email']),
                // This is quite intentional -- don't continue the loop
                false,
                // Force the message, even if logging is disabled
                true);
            return $this;
        }
        return $thread->postEmail($mailinfo, $this);
    }

    function getId() {
        return $this->id;
    }

    function getPid() {
        return $this->get('pid', 0);
    function getParent() {
        return $this->parent;
        return ThreadEntryBody::fromFormattedText($this->body, $this->format,
            array('balanced' => $this->hasFlag(self::FLAG_BALANCED))
        );
    function setBody($body) {
        global $cfg;

Peter Rotich's avatar
Peter Rotich committed
        if (!$body instanceof ThreadEntryBody) {
            if ($cfg->isRichTextEnabled())
Peter Rotich's avatar
Peter Rotich committed
                $body = new HtmlThreadEntryBody($body);
Peter Rotich's avatar
Peter Rotich committed
                $body = new TextThreadEntryBody($body);
        $this->format = $body->getType();
        $this->body = (string) $body;
        return $this->save();
    function getMessage() {
        return $this->getBody();
    }

    function getCreateDate() {
    }

    function getNumAttachments() {
        return $this->attachments->count();
    function getEmailMessageId() {
        if ($this->email_info)
            return $this->email_info->mid;
    function getEmailHeaderArray() {
        require_once(INCLUDE_DIR.'class.mailparse.php');

        if (!isset($this->_headers) && $this->email_info
            && isset($this->email_info->headers)
        ) {
            $this->_headers = Mail_Parse::splitHeaders($this->email_info->headers);
        }
        return $this->_headers;
    function getEmailReferences($include_mid=true) {
        $references = '';
        $headers = self::getEmailHeaderArray();
        if (isset($headers['References']) && $headers['References'])
            $references = $headers['References']." ";
        if ($include_mid && ($mid = $this->getEmailMessageId()))
            $references .= $mid;
        return $references;
    /**
     * Retrieve a list of all the recients of this message if the message
     * was received via email.
     *
     * Returns:
     * (array<RFC_822>) list of recipients parsed with the Mail/RFC822
     * address parsing utility. Returns an empty array if the message was
     * not received via email.
     */
    function getAllEmailRecipients() {
        $headers = self::getEmailHeaderArray();
        $recipients = array();
        if (!$headers)
            return $recipients;

        foreach (array('To', 'Cc') as $H) {
            if (!isset($headers[$H]))
                continue;

            if (!($all = Mail_Parse::parseAddressList($headers[$H])))
                continue;

            $recipients = array_merge($recipients, $all);
        }
        return $recipients;
    }

    /**
     * Recurse through the ancestry of this thread entry to find the first
     * thread entry which cites a email Message-ID field.
     *
     * Returns:
     * <ThreadEntry> or null if neither this thread entry nor any of its
     * ancestry contains an email header with an email Message-ID header.
     */
    function findOriginalEmailMessage() {
        $P = $this;
        while (!$P->getEmailMessageId()
            && ($P = $P->getParent()));
        return $P;
    }

    function getUIDFromEmailReference($ref) {

        $info = unpack('Vtid/Vuid',
                Base32::decode(strtolower(substr($ref, -13))));

        if ($info && $info['tid'] == $this->getId())
            return $info['uid'];

    }

Peter Rotich's avatar
Peter Rotich committed
    function getThreadId() {
Peter Rotich's avatar
Peter Rotich committed
    }

    function getThread() {
        if (!isset($this->_thread) && $this->thread_id)
            // TODO: Consider typing the thread based on its type field
            $this->_thread = ObjectThread::lookup($this->getThreadId());
        return isset($this->staff_id) ? $this->staff_id : 0;
    }

    function getStaff() {
        return $this->staff;
    }

    function getUserId() {
        return isset($this->user_id) ? $this->user_id : 0;
    function getEditor() {
        static $types = array(
            'U' => 'User',
            'S' => 'Staff',
        );
        if (!isset($types[$this->editor_type]))
            return null;
        return $types[$this->editor_type]::lookup($this->editor);
    }
    function getName() {
        if ($this->staff_id)
            return $this->staff->getName();
        if ($this->user_id)
            return $this->user->getName();
Peter Rotich's avatar
Peter Rotich committed
    function getEmailHeader() {
        if ($this->email_info)
            return $this->email_info->headers;
    function isAutoReply() {
        if (!isset($this->is_autoreply))
            $this->is_autoreply = $this->getEmailHeaderArray()
                ?  TicketFilter::isAutoReply($this->getEmailHeaderArray()) : false;
        return $this->is_autoreply;
    function isBounce() {
        if (!isset($this->is_bounce))
            $this->is_bounce = $this->getEmailHeaderArray()
                ? TicketFilter::isBounce($this->getEmailHeaderArray()) : false;
        return $this->is_bounce;
    function isBounceOrAutoReply() {
        return ($this->isAutoReply() || $this->isBounce());
    function hasFlag($flag) {
        return ($this->get('flags', 0) & $flag) != 0;
    }
    function clearFlag($flag) {
        return $this->set('flags', $this->get('flags') & ~$flag);
    }
    function setFlag($flag) {
        return $this->set('flags', $this->get('flags') | $flag);
    }

    function isSystem() {
        return $this->hasFlag(self::FLAG_SYSTEM);
    }

    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.',
        );
        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
                    && ($thread = $this->getThread())
                ) {
                    // Log to the thread directly, since alerts should be
                    // suppressed and this is defintely a system message
                    $thread->addNote(array(
                        'title' => _S('File Import Error'),
                        'note' => new TextThreadEntryBody($error),
                        'poster' => 'SYSTEM',
                        'staffId' => 0,
                    ));
                }
Peter Rotich's avatar
Peter Rotich committed
                continue;
            }

            if (is_string($name))
                $F['name'] = $name;
            if (isset($AF))
                $F['file'] = $AF;
            // Add things like the `key` field, but don't change current
            // keys of the file array
            if (is_array($file))
                $F += $file;
            // Key is required for CID rewriting in the body
            if (!isset($F['key']) && ($AF = AttachmentFile::lookup($F['id'])))
                $F['key'] = $AF->key;

Peter Rotich's avatar
Peter Rotich committed
    }

   /*
    Save attachment to the DB.
    @file is a mixed var - can be ID or file hashtable.
    */
    function createAttachment($file, $name=false) {
        $att = new Attachment(array(
            'type' => 'H',
            'object_id' => $this->getId(),
            'file_id' => $file['id'],
            'inline' => $file['inline'] ? 1 : 0,
        // Record varying file names in the attachment record
        if (is_array($file) && isset($file['name'])) {
            $filename = $file['name'];
        }
        elseif (is_string($name)) {
            $filename = $name;
        }
        if ($filename) {
            // This should be a noop since the ORM caches on PK
            $F = @$file['file'] ?: AttachmentFile::lookup($file['id']);
            // XXX: This is not Unicode safe
            if ($F && 0 !== strcasecmp($F->name, $filename))
                $att->name = $filename;
        }
        if (!$att->save())
            return false;
        return $att;
    function createAttachments(array $files) {
        $attachments = array();
        foreach ($files as $info) {
           if ($A = $this->createAttachment($info, @$info['name'] ?: false))
Peter Rotich's avatar
Peter Rotich committed
    }

    function getAttachments() {
        return $this->attachments;
    }

    function getAttachmentUrls() {
        foreach ($this->attachments as $att) {
            $json[$att->file->getKey()] = array(