Skip to content
Snippets Groups Projects
  • Jared Hancock's avatar
    9e75169e
    Dynamic data for osTicket · 9e75169e
    Jared Hancock authored
    *This is a major redesign / rework of the osTicket base*
    
    This patch drops the concept of static ticket metadata and allows for an
    admin-configurable arbitrary data that is attachable to tickets
    
    The system is architected such that the base osTicket install now comes with
    a "default" form that has fields for subject, name, email, and phone number.
    This form is editable to allow for the addition of arbitrary other fields;
    however, the basic fields must remain in order to be associated with a
    help-topic and attached to a ticket.
    
    This concept can be expanded to allow for arbitrary data associated with
    registered clients or ticket thread items.
    
    Forms are comprised of sections. Sections have a title and instructions
    properties and a list of fields. Fields have various implementations to
    represent different data such as text, long answer, phone number, datetime,
    yes/no, and selections, and are configurable to define the look and feel and
    interpretation of the respective form field.
    
    Dropdown lists are represented as "Dynamic Lists", which are
    admin-configurable lists of items. Dropdowns can be optionally represented
    as Bootstrap typeahead fields.
    
    This also adds the start of a simple ORM which will hopefully be expanded in
    the future to support multiple database platforms. Currently, only MySQL is
    implemented.
    9e75169e
    History
    Dynamic data for osTicket
    Jared Hancock authored
    *This is a major redesign / rework of the osTicket base*
    
    This patch drops the concept of static ticket metadata and allows for an
    admin-configurable arbitrary data that is attachable to tickets
    
    The system is architected such that the base osTicket install now comes with
    a "default" form that has fields for subject, name, email, and phone number.
    This form is editable to allow for the addition of arbitrary other fields;
    however, the basic fields must remain in order to be associated with a
    help-topic and attached to a ticket.
    
    This concept can be expanded to allow for arbitrary data associated with
    registered clients or ticket thread items.
    
    Forms are comprised of sections. Sections have a title and instructions
    properties and a list of fields. Fields have various implementations to
    represent different data such as text, long answer, phone number, datetime,
    yes/no, and selections, and are configurable to define the look and feel and
    interpretation of the respective form field.
    
    Dropdown lists are represented as "Dynamic Lists", which are
    admin-configurable lists of items. Dropdowns can be optionally represented
    as Bootstrap typeahead fields.
    
    This also adds the start of a simple ORM which will hopefully be expanded in
    the future to support multiple database platforms. Currently, only MySQL is
    implemented.
api.tickets.php 6.94 KiB
<?php

include_once INCLUDE_DIR.'class.api.php';
include_once INCLUDE_DIR.'class.ticket.php';

class TicketApiController extends ApiController {

    # Supported arguments -- anything else is an error. These items will be
    # inspected _after_ the fixup() method of the ApiXxxDataParser classes
    # so that all supported input formats should be supported
    function getRequestStructure($format, $data=null) {
        $supported = array(
            "alert", "autorespond", "source", "topicId",
            "attachments" => array("*" =>
                array("name", "type", "data", "encoding")
            ),
            "message", "ip", "priorityId"
        );
        # Fetch dynamic form field names for the given help topic and add
        # the names to the supported request structure
        if (isset($data['topicId'])) {
            $topic=Topic::lookup($data['topicId']);
            $formset=DynamicFormset::lookup($topic->ht['formset_id']);
            foreach ($formset->getForms() as $form)
                foreach ($form->getForm()->getFields() as $field)
                    $supported[] = $field->get('name');
        }

        if(!strcasecmp($format, 'email')) {
            $supported = array_merge($supported, array('header', 'mid',
                'emailId', 'ticketId', 'reply-to', 'reply-to-name',
                'in-reply-to', 'references'));
            $supported['attachments']['*'][] = 'cid';
        }

        return $supported;
    }

    /*
     Validate data - overwrites parent's validator for additional validations.
    */
    function validate(&$data, $format) {
        global $ost;

        //Call parent to Validate the structure
        if(!parent::validate($data, $format))
            $this->exerr(400, 'Unexpected or invalid data received');

        //Nuke attachments IF API files are not allowed.
        if(!$ost->getConfig()->allowAPIAttachments())
            $data['attachments'] = array();

        //Validate attachments: Do error checking... soft fail - set the error and pass on the request.
        if($data['attachments'] && is_array($data['attachments'])) {
            foreach($data['attachments'] as &$attachment) {
                if(!$ost->isFileTypeAllowed($attachment))
                    $attachment['error'] = 'Invalid file type (ext) for '.Format::htmlchars($attachment['name']);
                elseif ($attachment['encoding'] && !strcasecmp($attachment['encoding'], 'base64')) {
                    if(!($attachment['data'] = base64_decode($attachment['data'], true)))
                        $attachment['error'] = sprintf('%s: Poorly encoded base64 data', Format::htmlchars($attachment['name']));
                }
            }
            unset($attachment);
        }

        return true;
    }


    function create($format) {

        if(!($key=$this->requireApiKey()) || !$key->canCreateTickets())
            return $this->exerr(401, 'API key not authorized');

        $ticket = null;
        if(!strcasecmp($format, 'email')) {
            # Handle remote piped emails - could be a reply...etc.
            $ticket = $this->processEmail();
        } else {
            # Parse request body
            $ticket = $this->createTicket($this->getRequest($format));
        }

        if(!$ticket)
            return $this->exerr(500, "Unable to create new ticket: unknown error");

        $this->response(201, $ticket->getExtId());
    }

    /* private helper functions */

    function createTicket($data) {

        # Pull off some meta-data
        $alert = $data['alert'] ? $data['alert'] : true;
        $autorespond = $data['autorespond'] ? $data['autorespond'] : true;
        $data['source'] = $data['source'] ? $data['source'] : 'API';

        # Create the ticket with the data (attempt to anyway)
        $errors = array();

        $topic=Topic::lookup($data['topicId']);
        $forms=DynamicFormset::lookup($topic->ht['formset_id'])->getForms();
        foreach ($forms as $idx=>$f) {
            $forms[$idx] = $form = $f->getForm()->instanciate($f->sort);
            # Collect name, email address, and subject for banning and such
            foreach ($form->getFields() as $field) {
                $fname = $field->get('name');
                if ($fname && isset($data[$fname]))
                    $field->value = $data[$fname];
            }
            if (!$form->isValid())
                $errors = array_merge($errors, $form->errors());
        }

        $ticket = Ticket::create($data, $errors, $data['source'], $autorespond, $alert);
        # Return errors (?)
        if (count($errors)) {
            if(isset($errors['errno']) && $errors['errno'] == 403)
                return $this->exerr(403, 'Ticket denied');
            else
                return $this->exerr(
                        400,
                        "Unable to create new ticket: validation errors:\n"
                        .Format::array_implode(": ", "\n", $errors)
                        );
        } elseif (!$ticket) {
            return $this->exerr(500, "Unable to create new ticket: unknown error");
        }

        # Save dynamic forms
        foreach ($forms as $f) {
            $f->set('ticket_id', $ticket->getId());
            $f->save();
        }

        return $ticket;
    }

    function processEmail() {

        $data = $this->getEmailRequest();
        if($data['ticketId'] && ($ticket=Ticket::lookup($data['ticketId']))) {
            if(($msgid=$ticket->postMessage($data, 'Email')))
                return $ticket;
        }

        if (($thread = ThreadEntry::lookupByEmailHeaders($data))
                && $thread->postEmail($data)) {
            return $thread->getTicket();
        }
        return $this->createTicket($data);
    }

}

//Local email piping controller - no API key required!
class PipeApiController extends TicketApiController {

    //Overwrite grandparent's (ApiController) response method.
    function response($code, $resp) {

        //Use postfix exit codes - instead of HTTP
        switch($code) {
            case 201: //Success
                $exitcode = 0;
                break;
            case 400:
                $exitcode = 66;
                break;
            case 401: /* permission denied */
            case 403:
                $exitcode = 77;
                break;
            case 415:
            case 416:
            case 417:
            case 501:
                $exitcode = 65;
                break;
            case 503:
                $exitcode = 69;
                break;
            case 500: //Server error.
            default: //Temp (unknown) failure - retry
                $exitcode = 75;
        }

        //echo "$code ($exitcode):$resp";
        //We're simply exiting - MTA will take care of the rest based on exit code!
        exit($exitcode);
    }

    function  process() {
        $pipe = new PipeApiController();
        if(($ticket=$pipe->processEmail()))
           return $pipe->response(201, $ticket->getNumber());

        return $pipe->exerr(416, 'Request failed - retry again!');
    }
}

?>