Skip to content
Snippets Groups Projects
class.thread.php 83.1 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');
//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 $_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,
            ));
        }

        if ($cids) {
            $this->collaborators->filter(array(
                'thread_id' => $this->getId(),
                Q::not(array('id__in' => $cids))
            ))->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));

        // 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) {
        // +==================+===================+=============+
        // | 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?
            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' => $this,
            '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']);
            break;

        // 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);
            break;

        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],
                        $matches))
                continue;

            // 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;
            }
        }

        return null;
    }

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
            return false;

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;
    }

Peter Rotich's avatar
Peter Rotich committed
    static function create($vars) {
        $inst = parent::create($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 PERM_EDIT     = 'thread.edit';

    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) {
        if (!($thread = $this->getThread()))
            // Kind of hard to continue a discussion without a thread ...
            return false;

        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);
    }

    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);
    }

Peter Rotich's avatar
Peter Rotich committed
    //Web uploads - caller is expected to format, validate and set any errors.
    function uploadFiles($files) {

        if(!$files || !is_array($files))
            return false;

        $uploaded=array();
        foreach($files as $file) {
            if($file['error'] && $file['error']==UPLOAD_ERR_NO_FILE)
                continue;

            if(!$file['error']
                    && ($F=AttachmentFile::upload($file))
                    && $this->saveAttachment($F))
                $uploaded[]= $F->getId();
Peter Rotich's avatar
Peter Rotich committed
            else {
                if(!$file['error'])
                    $error = sprintf(__('Unable to upload file - %s'),$file['name']);
Peter Rotich's avatar
Peter Rotich committed
                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);
    function importAttachments(&$attachments) {
Peter Rotich's avatar
Peter Rotich committed

        if(!$attachments || !is_array($attachments))
            return null;

        $files = array();
        foreach($attachments as &$attachment)
Peter Rotich's avatar
Peter Rotich committed
            if(($id=$this->importAttachment($attachment)))
                $files[] = $id;

        return $files;
    }

    /* Emailed & API attachments handler */
    function importAttachment(&$attachment) {
Peter Rotich's avatar
Peter Rotich committed

        if(!$attachment || !is_array($attachment))
            return null;

        $A=null;
        if ($attachment['error'] || !($A=$this->saveAttachment($attachment))) {
Peter Rotich's avatar
Peter Rotich committed
            $error = $attachment['error'];
            if(!$error)
                $error = sprintf(_S('Unable to import attachment - %s'),
                        $attachment['name']);
Peter Rotich's avatar
Peter Rotich committed
            //FIXME: logComment here
            $this->getThread()->getObject()->logNote(
                    _S('File Import Error'), $error, _S('SYSTEM'), false);
Peter Rotich's avatar
Peter Rotich committed
    }

   /*
    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;

        $att = Attachment::create(array(
            'type' => 'H',
            'object_id' => $this->getId(),
            'file_id' => $fileId,
            'inline' => $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 = $F ?: AttachmentFile::lookup($fileId);
            // XXX: This is not Unicode safe
            if ($F && 0 !== strcasecmp($F->name, $filename))
        if (!$att->save())
            return false;
        return $att;
Peter Rotich's avatar
Peter Rotich committed
    }

    function saveAttachments($files) {
        foreach ($files as $name=>$file) {
           if (($A = $this->saveAttachment($file, $name)))
Peter Rotich's avatar
Peter Rotich committed
    }

    function getAttachments() {