diff --git a/ajax.php b/ajax.php index 8ea5226439f971b9cd917a13f53c099579394ed1..80e3a7838693ca355859285019aa861043b0b057 100644 --- a/ajax.php +++ b/ajax.php @@ -33,6 +33,7 @@ $dispatcher = patterns('', url_post('^(?P<id>\d+)$', 'updateDraftClient'), url_delete('^(?P<id>\d+)$', 'deleteDraftClient'), url_post('^(?P<id>\d+)/attach$', 'uploadInlineImageClient'), + url_post('^(?P<namespace>[\w.]+)/attach$', 'uploadInlineImageEarlyClient'), url_get('^(?P<namespace>[\w.]+)$', 'getDraftClient'), url_post('^(?P<namespace>[\w.]+)$', 'createDraftClient') )), diff --git a/include/ajax.config.php b/include/ajax.config.php index 733fa014d2c18af5e09d92c5b3ba497561f1b8ca..d2547d98c4abd967837d9a49073ff29b64bc5d32 100644 --- a/include/ajax.config.php +++ b/include/ajax.config.php @@ -13,6 +13,7 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ +require_once INCLUDE_DIR . 'class.ajax.php'; if(!defined('INCLUDE_DIR')) die('!'); @@ -42,7 +43,7 @@ class ConfigAjaxAPI extends AjaxController { return $this->json_encode($config); } - function client() { + function client($headers=true) { global $cfg; $lang = Internationalization::getCurrentLanguage(); @@ -62,8 +63,10 @@ class ConfigAjaxAPI extends AjaxController { ); $config = $this->json_encode($config); - Http::cacheable(md5($config), $cfg->lastModified()); - header('Content-Type: application/json; charset=UTF-8'); + if ($headers) { + Http::cacheable(md5($config), $cfg->lastModified()); + header('Content-Type: application/json; charset=UTF-8'); + } return $config; } diff --git a/include/ajax.draft.php b/include/ajax.draft.php index f727e2cc9a10040786f6abb3502a8a21f7e04806..5fc24e77a9c5e08d3e8607a6613c4e1a7fdaec6b 100644 --- a/include/ajax.draft.php +++ b/include/ajax.draft.php @@ -7,58 +7,40 @@ require_once(INCLUDE_DIR.'class.draft.php'); class DraftAjaxAPI extends AjaxController { function _createDraft($vars) { - $field_list = array('response', 'note', 'answer', 'body', - 'message', 'issue'); - foreach ($field_list as $field) { - if (isset($_POST[$field])) { - $vars['body'] = urldecode($_POST[$field]); - break; - } - } - if (!isset($vars['body'])) - return Http::response(422, "Draft body not found in request"); - - $errors = array(); - if (!($draft = Draft::create($vars, $errors))) - Http::response(500, print_r($errors, true)); - - // If the draft is created from an existing document, ensure inline - // attachments from the cloned document are attachned to the draft - // XXX: Actually, I think this is just wasting time, because the - // other object already has the items attached, so the database - // won't clean up the files. They don't have to be attached to - // the draft for Draft::getAttachmentIds to return the id of the - // attached file - //$draft->syncExistingAttachments(); + if (false === ($vars['body'] = self::_findDraftBody($_POST))) + return JsonDataEncoder::encode(array( + 'error' => __("Draft body not found in request"), + 'code' => 422, + )); + + if (!($draft = Draft::create($vars)) || !$draft->save()) + Http::response(500, 'Unable to create draft'); echo JsonDataEncoder::encode(array( 'draft_id' => $draft->getId(), )); } - function _getDraft($id) { - if (!($draft = Draft::lookup($id))) + function _getDraft($draft) { + if (!$draft || !$draft instanceof Draft) Http::response(205, "Draft not found. Create one first"); $body = Format::viewableImages($draft->getBody()); echo JsonDataEncoder::encode(array( 'body' => $body, - 'draft_id' => (int)$id, + 'draft_id' => $draft->getId(), )); } function _updateDraft($draft) { - $field_list = array('response', 'note', 'answer', 'body', - 'message', 'issue'); - foreach ($field_list as $field) { - if (isset($_POST[$field])) { - $body = urldecode($_POST[$field]); - break; - } - } - if (!isset($body)) - return Http::response(422, "Draft body not found in request"); + if (false === ($body = self::_findDraftBody($_POST))) + return JsonDataEncoder::encode(array( + 'error' => array( + 'message' => "Draft body not found in request", + 'code' => 422, + ) + )); if (!$draft->setBody($body)) return Http::response(500, "Unable to update draft body"); @@ -129,6 +111,8 @@ class DraftAjaxAPI extends AjaxController { echo JsonDataEncoder::encode(array( 'content_id' => 'cid:'.$f->getKey(), + // Return draft_id to connect the auto draft creation + 'draft_id' => $draft->getId(), 'filelink' => sprintf('image.php?h=%s', $f->getDownloadHash()) )); } @@ -141,30 +125,36 @@ class DraftAjaxAPI extends AjaxController { Http::response(403, "Valid session required"); $vars = array( - 'staff_id' => ($thisclient) ? $thisclient->getId() : 0, + 'staff_id' => ($thisclient) ? $thisclient->getId() : 1<<31, 'namespace' => $namespace, ); - $info = self::_createDraft($vars); - $info['draft_id'] = $namespace; + return self::_createDraft($vars); } function getDraftClient($namespace) { global $thisclient; if ($thisclient) { - if (!($id = Draft::findByNamespaceAndStaff($namespace, - $thisclient->getId()))) + try { + $draft = Draft::lookupByNamespaceAndStaff($namespace, + $thisclient->getId()); + } + catch (DoesNotExist $e) { Http::response(205, "Draft not found. Create one first"); + } } else { if (substr($namespace, -12) != substr(session_id(), -12)) Http::response(404, "Draft not found"); - elseif (!($id = Draft::findByNamespaceAndStaff($namespace, 0))) + try { + $draft = Draft::lookupByNamespaceAndStaff($namespace, 0); + } + catch (DoesNotExist $e) { Http::response(205, "Draft not found. Create one first"); + } } - - return self::_getDraft($id); + return self::_getDraft($draft); } function updateDraftClient($id) { @@ -220,6 +210,22 @@ class DraftAjaxAPI extends AjaxController { return self::_uploadInlineImage($draft); } + function uploadInlineImageEarlyClient($namespace) { + global $thisclient; + + if (!$thisclient && substr($namespace, -12) != substr(session_id(), -12)) + Http::response(403, "Valid session required"); + + $draft = Draft::create(array( + 'staff_id' => ($thisclient) ? $thisclient->getId() : 1<<31, + 'namespace' => $namespace, + )); + if (!$draft->save()) + Http::response(500, 'Unable to create draft'); + + return $this->uploadInlineImageClient($draft->getId()); + } + // Staff interface for drafts ======================================== function createDraft($namespace) { global $thisstaff; @@ -240,11 +246,15 @@ class DraftAjaxAPI extends AjaxController { if (!$thisstaff) Http::response(403, "Login required for draft creation"); - elseif (!($id = Draft::findByNamespaceAndStaff($namespace, - $thisstaff->getId()))) + try { + $draft = Draft::lookupByNamespaceAndStaff($namespace, + $thisstaff->getId()); + } + catch (DoesNotExist $e) { Http::response(205, "Draft not found. Create one first"); + } - return self::_getDraft($id); + return self::_getDraft($draft); } function updateDraft($id) { @@ -273,6 +283,22 @@ class DraftAjaxAPI extends AjaxController { return self::_uploadInlineImage($draft); } + function uploadInlineImageEarly($namespace) { + global $thisstaff; + + if (!$thisstaff) + Http::response(403, "Login required for image upload"); + + $draft = Draft::create(array( + 'staff_id' => $thisstaff->getId(), + 'namespace' => $namepace + )); + if (!$draft->save()) + Http::response(500, 'Unable to create draft'); + + return $this->uploadInlineImage($draft->getId()); + } + function deleteDraft($id) { global $thisstaff; @@ -320,5 +346,27 @@ class DraftAjaxAPI extends AjaxController { echo JsonDataEncoder::encode($files); } + function _findDraftBody($vars) { + if (isset($vars['name'])) { + $parts = array(); + if (preg_match('`(\w+)(?:\[(\w+)\])?(?:\[(\w+)\])?`', $_POST['name'], $parts)) { + array_shift($parts); + $focus = $vars; + foreach ($parts as $p) + $focus = $focus[$p]; + return urldecode($focus); + } + } + $field_list = array('response', 'note', 'answer', 'body', + 'message', 'issue'); + foreach ($field_list as $field) { + if (isset($vars[$field])) { + return urldecode($vars[$field]); + } + } + + return false; + } + } ?> diff --git a/include/class.draft.php b/include/class.draft.php index cdb5d43f440081d3e047993dd0df006cf23c39d6..fae8fd8d72389d6c37f2dc2892e564a99f246dec 100644 --- a/include/class.draft.php +++ b/include/class.draft.php @@ -1,28 +1,59 @@ <?php -class Draft { - - var $id; - var $ht; - - var $_attachments; - - function Draft($id) { - $this->id = $id; - $this->load(); - } - - function load() { - $this->attachments = new GenericAttachments($this->id, 'D'); - $sql = 'SELECT * FROM '.DRAFT_TABLE.' WHERE id='.db_input($this->id); - return (($res = db_query($sql)) - && ($this->ht = db_fetch_array($res))); +/** + * Class: Draft + * + * Defines a simple draft-saving mechanism for osTicket which supports draft + * fetch and update via an ajax mechanism (include/ajax.draft.php). + * + * Fields: + * id - (int:auto:pk) Draft ID number + * body - (text) Body of the draft + * namespace - (string) Identifier of draft grouping — useful for multiple + * drafts on the same document by different users + * staff_id - (int:null) Staff owner of the draft + * extra - (text:json) Extra attributes of the draft + * created - (date) Date draft was initially created + * updated - (date:null) Date draft was last updated + */ +class Draft extends VerySimpleModel { + + static $meta = array( + 'table' => DRAFT_TABLE, + 'pk' => array('id'), + ); + + var $attachments; + + function __construct() { + call_user_func_array(array('parent', '__construct'), func_get_args()); + if (isset($this->id)) + $this->attachments = new GenericAttachments($this->id, 'D'); } function getId() { return $this->id; } - function getBody() { return $this->ht['body']; } - function getStaffId() { return $this->ht['staff_id']; } - function getNamespace() { return $this->ht['namespace']; } + function getBody() { return $this->body; } + function getStaffId() { return $this->staff_id; } + function getNamespace() { return $this->namespace; } + + static function getDraftAndDataAttrs($namespace, $id=0, $original='') { + $draft_body = null; + $attrs = array(sprintf('data-draft-namespace="%s"', Format::htmlchars($namespace))); + $criteria = array('namespace'=>$namespace); + if ($id) { + $attrs[] = sprintf('data-draft-object-id="%s"', Format::htmlchars($id)); + $criteria['namespace'] .= '.' . $id; + } + if ($draft = static::lookup($criteria)) { + $attrs[] = sprintf('data-draft-id="%s"', $draft->getId()); + $draft_body = $draft->getBody(); + } + $attrs[] = sprintf('data-draft-original="%s"', + Format::htmlchars(Format::viewableImages($original))); + + return array(Format::htmlchars(Format::viewableImages($draft_body)), + implode(' ', $attrs)); + } function getAttachmentIds($body=false) { $attachments = array(); @@ -63,69 +94,49 @@ class Draft { function setBody($body) { // Change image.php urls back to content-id's $body = Format::sanitize($body, false); - $this->ht['body'] = $body; - $sql='UPDATE '.DRAFT_TABLE.' SET updated=NOW()' - .',body='.db_input($body) - .' WHERE id='.db_input($this->getId()); - return db_query($sql) && db_affected_rows() == 1; + $this->body = $body ?: ' '; + $this->updated = SqlFunction::NOW(); + return $this->save(); } function delete() { $this->attachments->deleteAll(); - $sql = 'DELETE FROM '.DRAFT_TABLE - .' WHERE id='.db_input($this->getId()); - return (db_query($sql) && db_affected_rows() == 1); + return parent::delete(); } - function save($id, $vars, &$errors) { + function isValid() { // Required fields - if (!$vars['namespace'] || !isset($vars['body']) || !isset($vars['staff_id'])) - return false; + return $this->namespace && isset($this->staff_id); + } - $sql = ' SET `namespace`='.db_input($vars['namespace']) - .' ,body='.db_input(Format::sanitize($vars['body'], false)) - .' ,staff_id='.db_input($vars['staff_id']); + function save($refetch=false) { + if (!$this->isValid()) + return false; - if (!$id) { - $sql = 'INSERT INTO '.DRAFT_TABLE.$sql - .' ,created=NOW()'; - if(!db_query($sql) || !($draft=self::lookup(db_insert_id()))) - return false; + return parent::save($refetch); + } - // Cloned attachments... - if($vars['attachments'] && is_array($vars['attachments'])) - $draft->attachments->upload($vars['attachments'], true); + static function create($vars) { + $attachments = @$vars['attachments']; + unset($vars['attachments']); - return $draft; - } - else { - $sql = 'UPDATE '.DRAFT_TABLE.$sql - .' WHERE id='.db_input($id); - if (db_query($sql) && db_affected_rows() == 1) - return $this; - } - } + $vars['created'] = SqlFunction::NOW(); + $draft = parent::create($vars); - function create($vars, &$errors) { - return self::save(0, $vars, $errors); - } + // Cloned attachments ... + if (false && $attachments && is_array($attachments)) + // XXX: This won't work until the draft is saved + $draft->attachments->upload($attachments, true); - function lookup($id) { - return ($id && is_numeric($id) - && ($d = new Draft($id)) - && $d->getId()==$id - ) ? $d : null; + return $draft; } - function findByNamespaceAndStaff($namespace, $staff_id) { - $sql = 'SELECT id FROM '.DRAFT_TABLE - .' WHERE `namespace`='.db_input($namespace) - .' AND staff_id='.db_input($staff_id); - if (($res = db_query($sql)) && (list($id) = db_fetch_row($res))) - return $id; - else - return false; + static function lookupByNamespaceAndStaff($namespace, $staff_id) { + return static::lookup(array( + 'namespace'=>$namespace, + 'staff_id'=>$staff_id + )); } /** @@ -134,8 +145,7 @@ class Draft { * closing a ticket, the staff_id should be left null so that all drafts * are cleaned up. */ - /* static */ - function deleteForNamespace($namespace, $staff_id=false) { + static function deleteForNamespace($namespace, $staff_id=false) { $sql = 'DELETE attach FROM '.ATTACHMENT_TABLE.' attach INNER JOIN '.DRAFT_TABLE.' draft ON (attach.object_id = draft.id AND attach.`type`=\'D\') @@ -145,11 +155,10 @@ class Draft { if (!db_query($sql)) return false; - $sql = 'DELETE FROM '.DRAFT_TABLE - .' WHERE `namespace` LIKE '.db_input($namespace); + $criteria = array('namespace__like'=>$namespace); if ($staff_id) - $sql .= ' AND staff_id='.db_input($staff_id); - return (!db_query($sql) || !db_affected_rows()); + $criteria['staff_id'] = $staff_id; + return static::objects()->filter($criteria)->delete(); } static function cleanup() { diff --git a/include/class.forms.php b/include/class.forms.php index c80c34ba1567cea62f7cb38150e11b020ca39228..73afeaa0eca493abcfe50d8d415af01bbd03f89d 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -2014,20 +2014,25 @@ class ThreadEntryWidget extends Widget { function render($client=null) { global $cfg; + $object_id = false; + if (!$client) { + $namespace = 'ticket.staff'; + } + else { + $namespace = 'ticket.client'; + $object_id = substr(session_id(), -12); + } + list($draft, $attrs) = Draft::getDraftAndDataAttrs($namespace, $object_id, $this->value); ?><div style="margin-bottom:0.5em;margin-top:0.5em"><strong><?php echo Format::htmlchars($this->field->get('label')); ?></strong>:</div> + <textarea style="width:100%;" name="<?php echo $this->field->get('name'); ?>" placeholder="<?php echo Format::htmlchars($this->field->get('hint')); ?>" - <?php if (!$client) { ?> - data-draft-namespace="ticket.staff" - <?php } else { ?> - data-draft-namespace="ticket.client" - data-draft-object-id="<?php echo substr(session_id(), -12); ?>" - <?php } ?> - class="richtext draft draft-delete ifhtml" + class="<?php if ($cfg->isHtmlThreadEnabled()) echo 'richtext'; + ?> draft draft-delete" <?php echo $attrs; ?> cols="21" rows="8" style="width:80%;"><?php echo - Format::htmlchars($this->value); ?></textarea> + $draft ?: Format::htmlchars($this->value); ?></textarea> <?php $config = $this->field->getConfiguration(); if (!$config['attachments']) diff --git a/include/client/footer.inc.php b/include/client/footer.inc.php index 9ff4ad15265c68a7a7938535786cae3ea9f910db..9521fbfc73a61d03735241bf4a6890b98c13b4a0 100644 --- a/include/client/footer.inc.php +++ b/include/client/footer.inc.php @@ -14,5 +14,12 @@ if (($lang = Internationalization::getCurrentLanguage()) && $lang != 'en_US') { <script type="text/javascript" src="ajax.php/i18n/<?php echo $lang; ?>/js"></script> <?php } ?> +<script type="text/javascript"> + getConfig().resolve(<?php + include INCLUDE_DIR . 'ajax.config.php'; + $api = new ConfigAjaxAPI(); + print $api->client(false); + ?>); +</script> </body> </html> diff --git a/include/client/view.inc.php b/include/client/view.inc.php index dc3a0419f97a353471b6a87f7dc4ad1453b5b881..efebfe775a2945433fe1c06bf5665d9171f4bc19 100644 --- a/include/client/view.inc.php +++ b/include/client/view.inc.php @@ -172,9 +172,13 @@ if (!$ticket->isClosed() || $ticket->isReopenable()) { ?> <span id="msg"><em><?php echo $msg; ?> </em></span><font class="error">* <?php echo $errors['message']; ?></font> <br/> <textarea name="message" id="message" cols="50" rows="9" wrap="soft" - data-draft-namespace="ticket.client" - data-draft-object-id="<?php echo $ticket->getId(); ?>" - class="richtext ifhtml draft"><?php echo $info['message']; ?></textarea> + class="<?php if ($cfg->isHtmlThreadEnabled()) echo 'richtext'; + ?> draft" <?php + list($draft, $attrs) = Draft::getDraftAndDataAttrs('ticket.client', $ticket->getId(), $info['message']); + echo $attrs; ?>><?php echo $draft ?: $info['message']; + ?></textarea> + </td> + </tr> <?php if ($messageField->isAttachmentsEnabled()) { ?> <?php diff --git a/include/staff/cannedresponse.inc.php b/include/staff/cannedresponse.inc.php index 6be5a63b1e7b2834971715c939e1e3073af0d78c..ad0d784fb5e0babcce5366938c7d0d5516dbe0fa 100644 --- a/include/staff/cannedresponse.inc.php +++ b/include/staff/cannedresponse.inc.php @@ -83,10 +83,12 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); (<a class="tip" href="#ticket_variables"><?php echo __('Supported Variables'); ?></a>) </div> <textarea name="response" class="richtext draft draft-delete" cols="21" rows="12" - data-draft-namespace="canned" - data-draft-object-id="<?php if (isset($canned)) echo $canned->getId(); ?>" - style="width:98%;" class="richtext draft"><?php - echo $info['response']; ?></textarea> + style="width:98%;" class="richtext draft" <?php + list($draft, $attrs) = Draft::getDraftAndDataAttrs('canned', + is_object($canned) ? $canned->getId() : false, $info['response']); + echo $attrs; ?>><?php echo $draft ?: $info['response']; + ?></textarea> + <br><br> <div><h3><?php echo __('Canned Attachments'); ?> <?php echo __('(optional)'); ?> <i class="help-tip icon-question-sign" href="#canned_attachments"></i></h3> <div class="error"><?php echo $errors['files']; ?></div> diff --git a/include/staff/faq.inc.php b/include/staff/faq.inc.php index 4fcea8138b348ae8bdd24ddd3316cc4e940826ab..ee27bfab24df1d25ba9804b5c28be058cbc9cae8 100644 --- a/include/staff/faq.inc.php +++ b/include/staff/faq.inc.php @@ -88,10 +88,11 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <b><?php echo __('Answer');?></b> <font class="error">* <?php echo $errors['answer']; ?></font></div> </div> <textarea name="answer" cols="21" rows="12" - style="width:98%;" class="richtext draft" - data-draft-namespace="faq" - data-draft-object-id="<?php if (is_object($faq)) echo $faq->getId(); ?>" - ><?php echo $info['answer']; ?></textarea> + style="width:98%;" class="richtext draft" <?php + list($draft, $attrs) = Draft::getDraftAndDataAttrs('faq', + is_object($faq) ? $faq->getId() : false, $info['answer']); + echo $attrs; ?>><?php echo $draft ?: $info['answer']; + ?></textarea> </td> </tr> <tr> diff --git a/include/staff/page.inc.php b/include/staff/page.inc.php index 532bfc0af8c4c9b9435b9be741ec01c433e2f3c9..49f2cbea471ad5fa51e3cd9fa18092b076581070 100644 --- a/include/staff/page.inc.php +++ b/include/staff/page.inc.php @@ -105,13 +105,14 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <tr> <td colspan=2 style="padding-left:3px;"> <textarea name="body" cols="21" rows="12" style="width:98%;" class="richtext draft" - data-draft-namespace="page" data-draft-object-id="<?php echo $info['id']; ?>" - ><?php echo $info['body']; ?></textarea> +<?php + list($draft, $attrs) = Draft::getDraftAndDataAttrs('page', $info['id'], $info['body']); + echo $attrs; ?>><?php echo $draft ?: $info['body']; ?></textarea> </td> </tr> <tr> <th colspan="2"> - <em><strong><?php echo __('Internal Notes'); ?></strong>: + <em><strong><?php echo __('Internal Notes'); ?></strong>: <?php echo __("be liberal, they're internal"); ?></em> </th> </tr> diff --git a/include/staff/templates/ticket-status.tmpl.php b/include/staff/templates/ticket-status.tmpl.php index 5e0d84ae3c82be28c5880b5444ffc65ea513a279..eae1b3272e99c586e0abdf88e08bdf9995a6c83b 100644 --- a/include/staff/templates/ticket-status.tmpl.php +++ b/include/staff/templates/ticket-status.tmpl.php @@ -84,7 +84,8 @@ $action = $info['action'] ?: ('#tickets/status/'. $state); ?> <textarea name="comments" id="comments" cols="50" rows="3" wrap="soft" style="width:100%" - class="richtext ifhtml no-bar" + class="<?php if ($cfg->isHtmlThreadEnabled()) echo 'richtext'; + ?> no-bar" placeholder="<?php echo $placeholder; ?>"><?php echo $info['comments']; ?></textarea> </td> diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php index 39acb0864519519c278c7c4d28771476a4042c20..d70af212e8d692ada02827e3423978bb4d28ed3d 100644 --- a/include/staff/ticket-open.inc.php +++ b/include/staff/ticket-open.inc.php @@ -302,13 +302,17 @@ if ($_POST) $signature = ''; if ($thisstaff->getDefaultSignatureType() == 'mine') $signature = $thisstaff->getSignature(); ?> - <textarea class="richtext ifhtml draft draft-delete" - data-draft-namespace="ticket.staff.response" - data-signature="<?php + <textarea + class="<?php if ($cfg->isHtmlThreadEnabled()) echo 'richtext'; + ?> draft draft-delete" data-signature="<?php echo Format::htmlchars(Format::viewableImages($signature)); ?>" data-signature-field="signature" data-dept-field="deptId" placeholder="<?php echo __('Initial response for the ticket'); ?>" name="response" id="response" cols="21" rows="8" + style="width:80%;" <?php + list($draft, $attrs) = Draft::getDraftAndDataAttrs('ticket.staff.response', false, $info['response']); + echo $attrs; ?>><?php echo $draft ?: $info['response']; + ?></textarea> style="width:80%;"><?php echo $info['response']; ?></textarea> <div class="attachments"> <?php @@ -371,11 +375,14 @@ print $response_form->getField('attachments')->render(); </tr> <tr> <td colspan=2> - <textarea class="richtext ifhtml draft draft-delete" + <textarea + class="<?php if ($cfg->isHtmlThreadEnabled()) echo 'richtext'; + ?> draft draft-delete" placeholder="<?php echo __('Optional internal note (recommended on assignment)'); ?>" - data-draft-namespace="ticket.staff.note" name="note" - cols="21" rows="6" style="width:80%;" - ><?php echo $info['note']; ?></textarea> + name="note" cols="21" rows="6" style="width:80%;" <?php + list($draft, $attrs) = Draft::getDraftAndDataAttrs('ticket.staff.note', false, $info['note']); + echo $attrs; ?>><?php echo $draft ?: $info['note']; + ?></textarea> </td> </tr> </tbody> diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index 078af3a0a268bf45bed22653fcbecfdaef703ba2..83f7622edf35589db3d2e267977b42b3e738f822 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -561,17 +561,18 @@ $tcount+= $ticket->getNumNotes(); } ?> <input type="hidden" name="draft_id" value=""/> <textarea name="response" id="response" cols="50" - data-draft-namespace="ticket.response" data-signature-field="signature" data-dept-id="<?php echo $dept->getId(); ?>" data-signature="<?php echo Format::htmlchars(Format::viewableImages($signature)); ?>" placeholder="<?php echo __( 'Start writing your response here. Use canned responses from the drop-down above' ); ?>" - data-draft-object-id="<?php echo $ticket->getId(); ?>" rows="9" wrap="soft" - class="richtext ifhtml draft draft-delete"><?php - echo $info['response']; ?></textarea> + class="<?php if ($cfg->isHtmlThreadEnabled()) echo 'richtext'; + ?> draft draft-delete" <?php + list($draft, $attrs) = Draft::getDraftAndDataAttrs('ticket.response', $ticket->getId(), $info['response']); + echo $attrs; ?>><?php echo $draft ?: $info['response']; + ?></textarea> <div id="reply_form_attachments" class="attachments"> <?php print $response_form->getField('attachments')->render(); @@ -671,9 +672,11 @@ print $response_form->getField('attachments')->render(); <div class="error"><?php echo $errors['note']; ?></div> <textarea name="note" id="internal_note" cols="80" placeholder="<?php echo __('Note details'); ?>" - rows="9" wrap="soft" data-draft-namespace="ticket.note" - data-draft-object-id="<?php echo $ticket->getId(); ?>" - class="richtext ifhtml draft draft-delete"><?php echo $info['note']; + rows="9" wrap="soft" + class="<?php if ($cfg->isHtmlThreadEnabled()) echo 'richtext'; + ?> draft draft-delete" <?php + list($draft, $attrs) = Draft::getDraftAndDataAttrs('ticket.note', $ticket->getId(), $info['note']); + echo $attrs; ?>><?php echo $draft ?: $info['note']; ?></textarea> <div class="attachments"> <?php @@ -764,7 +767,8 @@ print $note_form->getField('attachments')->render(); <td> <textarea name="transfer_comments" id="transfer_comments" placeholder="<?php echo __('Enter reasons for the transfer'); ?>" - class="richtext ifhtml no-bar" cols="80" rows="7" wrap="soft"><?php + class="<?php if ($cfg->isHtmlThreadEnabled()) echo 'richtext'; + ?> no-bar" cols="80" rows="7" wrap="soft"><?php echo $info['transfer_comments']; ?></textarea> <span class="error"><?php echo $errors['transfer_comments']; ?></span> </td> @@ -861,7 +865,8 @@ print $note_form->getField('attachments')->render(); <textarea name="assign_comments" id="assign_comments" cols="80" rows="7" wrap="soft" placeholder="<?php echo __('Enter reasons for the assignment or instructions for assignee'); ?>" - class="richtext ifhtml no-bar"><?php echo $info['assign_comments']; ?></textarea> + class="<?php if ($cfg->isHtmlThreadEnabled()) echo 'richtext'; + ?> no-bar"><?php echo $info['assign_comments']; ?></textarea> <span class="error"><?php echo $errors['assign_comments']; ?></span><br> </td> </tr> diff --git a/include/staff/tpl.inc.php b/include/staff/tpl.inc.php index fe048decd06309b9f2f3383aa3950bb0bcbed687..37e29177e80d850efb54bbd6e8a0c79c891db421 100644 --- a/include/staff/tpl.inc.php +++ b/include/staff/tpl.inc.php @@ -108,9 +108,10 @@ $tpl=$msgtemplates[$selected]; </div> <input type="hidden" name="draft_id" value=""/> <textarea name="body" cols="21" rows="16" style="width:98%;" wrap="soft" - data-toolbar-external="#toolbar" - class="richtext draft" data-draft-namespace="tpl.<?php echo Format::htmlchars($selected); ?>" - data-draft-object-id="<?php echo $tpl_id; ?>"><?php echo $info['body']; ?></textarea> + data-toolbar-external="#toolbar" class="richtext draft" <?php + list($draft, $attrs) = Draft::getDraftAndDataAttrs('tpl.'.$selected, $tpl_id, $info['body']); + echo $attrs; ?>><?php echo $draft ?: $info['body']; + ?></textarea> </div> <p style="text-align:center"> diff --git a/js/osticket.js b/js/osticket.js index f4aeebd1a1075541fadb34c70f4f49ce6fd70452..911cbf4e758ab7b185837dfe8a51855e692d48c4 100644 --- a/js/osticket.js +++ b/js/osticket.js @@ -78,39 +78,6 @@ $(document).ready(function(){ }); - getConfig = (function() { - var dfd = $.Deferred(), - requested = false; - return function() { - if (dfd.state() != 'resolved' && !requested) - requested = $.ajax({ - url: "ajax.php/config/client", - dataType: 'json', - success: function (json_config) { - dfd.resolve(json_config); - } - }); - return dfd; - } - })(); - - $.translate_format = function(str) { - var translation = { - 'd':'dd', - 'j':'d', - 'z':'o', - 'm':'mm', - 'F':'MM', - 'n':'m', - 'Y':'yy' - }; - // Change PHP formats to datepicker ones - $.each(translation, function(php, jqdp) { - str = str.replace(php, jqdp); - }); - return str; - }; - var showNonLocalImage = function(div) { var $div = $(div), $img = $div.append($('<img>') @@ -199,7 +166,32 @@ showImagesInline = function(urls, thread_id) { e.data('wrapped', true); } }); -} +}; + +getConfig = (function() { + var dfd = $.Deferred(), + requested = false; + return function() { + return dfd; + }; +})(); + +$.translate_format = function(str) { + var translation = { + 'd':'dd', + 'j':'d', + 'z':'o', + 'm':'mm', + 'F':'MM', + 'n':'m', + 'Y':'yy' + }; + // Change PHP formats to datepicker ones + $.each(translation, function(php, jqdp) { + str = str.replace(php, jqdp); + }); + return str; +}; $.sysAlert = function (title, msg, cb) { var $dialog = $('.dialog#alert'); diff --git a/js/redactor-osticket.js b/js/redactor-osticket.js index cded2e1f3ffdc7d425428155eec3cf2a0815777d..6e50a61e399c697ab7fe16e7dabf1dddf7ab4ff5 100644 --- a/js/redactor-osticket.js +++ b/js/redactor-osticket.js @@ -21,10 +21,24 @@ RedactorPlugins.draft = { var autosave_url = 'ajax.php/draft/' + this.opts.draftNamespace; if (this.opts.draftObjectId) autosave_url += '.' + this.opts.draftObjectId; - this.opts.autosave = autosave_url; - this.opts.autosaveInterval = 10; - this.opts.autosaveCallback = this.setupDraftUpdate; - this.opts.initCallback = this.recoverDraft; + this.opts.autosave = this.opts.autoCreateUrl = autosave_url; + this.opts.autosaveInterval = 30; + this.opts.autosaveCallback = this.afterUpdateDraft; + this.opts.autosaveErrorCallback = this.autosaveFailed; + this.opts.imageUploadErrorCallback = this.displayError; + if (this.opts.draftId) { + this.opts.autosave = 'ajax.php/draft/'+this.opts.draftId; + this.opts.clipboardUploadUrl = + this.opts.imageUpload = + 'ajax.php/draft/'+this.opts.draftId+'/attach'; + } + else { + // Just upload the file. A draft will be created automatically + // and will be configured locally in the afterUpateDraft() + this.opts.clipboardUploadUrl = + this.opts.imageUpload = this.opts.autoCreateUrl + '/attach'; + this.opts.imageUploadCallback = this.afterUpdateDraft; + } this.$draft_saved = $('<span>') .addClass("pull-right draft-saved") @@ -33,73 +47,60 @@ RedactorPlugins.draft = { .text(__('Draft Saved'))); // Float the [Draft Saved] box with the toolbar this.$toolbar.append(this.$draft_saved); + // Add [Delete Draft] button to the toolbar if (this.opts.draftDelete) { - var trash = this.buttonAdd('deleteDraft', __('Delete Draft'), this.deleteDraft); + var trash = this.draftDeleteButton = + this.buttonAdd('deleteDraft', __('Delete Draft'), + this.deleteDraft); this.buttonAwesome('deleteDraft', 'icon-trash'); trash.parent().addClass('pull-right'); trash.addClass('delete-draft'); + if (!this.opts.draftId) + trash.hide(); } }, - recoverDraft: function() { - var self = this; - $.ajax(this.opts.autosave, { - dataType: 'json', - statusCode: { - 200: function(json) { - self.draft_id = json.draft_id; - // Replace the current content with the draft, sync, and make - // images editable - self.setupDraftUpdate(json); - if (!json.body) return; - self.set(json.body, false); - self.observeStart(); - }, - 205: function() { - // Save empty draft immediately; - var ai = self.opts.autosaveInterval; - - // Save immediately -- capture the created autosave - // interval and clear it as soon as possible. Note that - // autosave()ing doesn't happen immediately. It happens - // async after the autosaveInterval expires. - self.opts.autosaveInterval = 0; - self.autosave(); - var interval = self.autosaveInterval; - setTimeout(function() { - clearInterval(interval); - }, 1); - - // Reinstate previous autosave interval timing - self.opts.autosaveInterval = ai; - } - } - }); - }, - setupDraftUpdate: function(data) { - if (this.get()) - this.$draft_saved.show().delay(5000).fadeOut(); - + afterUpdateDraft: function(data) { // Slight workaround. Signal the 'keyup' event normally signaled // from typing in the <textarea> - if ($.autoLock && this.opts.draftNamespace == 'ticket.response') + if ($.autoLock && this.opts.draftNamespace == 'ticket.response') { if (this.get()) $.autoLock.handleEvent(); + } - if (typeof data != 'object') - data = $.parseJSON(data); - - if (!data || !data.draft_id) + // If the draft was created, a draft_id will be sent back — update + // the URL to send updates in the future + if (!this.opts.draftId && data.draft_id) { + this.opts.draftId = data.draft_id; + this.opts.autosave = 'ajax.php/draft/' + data.draft_id; + this.opts.clipboardUploadUrl = + this.opts.imageUpload = + 'ajax.php/draft/'+this.opts.draftId+'/attach'; + if (!this.get()) + this.set(' ', false); + } + // Only show the [Draft Saved] notice if there is content in the + // field that has been touched + if (!this.firstSave) { + this.firstSave = true; + // No change yet — dont't show the button + return; + } + if (data && this.get()) { + this.$draft_saved.show().delay(5000).fadeOut(); + } + // Show the button if there is a draft to delete + if (this.opts.draftId && this.opts.draftDelete) + this.draftDeleteButton.show(); + }, + autosaveFailed: function(error) { + if (error.code == 422) + // Unprocessable request (Empty message) return; - $('input[name=draft_id]', this.$box.closest('form')) - .val(data.draft_id); - this.draft_id = data.draft_id; - this.opts.clipboardUploadUrl = - this.opts.imageUpload = - 'ajax.php/draft/'+data.draft_id+'/attach'; - this.opts.imageUploadErrorCallback = this.displayError; - this.opts.original_autosave = this.opts.autosave; - this.opts.autosave = 'ajax.php/draft/'+data.draft_id; + this.displayError(error); + // Cancel autosave + clearInterval(this.autosaveInterval); + this.hideDraftSaved(); }, displayError: function(json) { @@ -111,18 +112,20 @@ RedactorPlugins.draft = { }, deleteDraft: function() { - if (!this.draft_id) + if (!this.opts.draftId) // Nothing to delete return; var self = this; - $.ajax('ajax.php/draft/'+this.draft_id, { + $.ajax('ajax.php/draft/'+this.opts.draftId, { type: 'delete', async: false, success: function() { - self.draft_id = undefined; + self.draft_id = self.opts.draftId = undefined; self.hideDraftSaved(); - self.set('', false, false); - self.opts.autosave = self.opts.original_autosave; + self.set(self.opts.draftOriginal || '', false, false); + self.opts.autosave = self.opts.autoCreateUrl; + self.draftDeleteButton.hide(); + self.firstSave = false; } }); } @@ -141,10 +144,10 @@ RedactorPlugins.signature = { else this.$signatureBox.hide(); $('input[name='+$el.data('signatureField')+']', $el.closest('form')) - .on('change', false, false, $.proxy(this.updateSignature, this)) + .on('change', false, false, $.proxy(this.updateSignature, this)); if ($el.data('deptField')) $(':input[name='+$el.data('deptField')+']', $el.closest('form')) - .on('change', false, false, $.proxy(this.updateSignature, this)) + .on('change', false, false, $.proxy(this.updateSignature, this)); // Expand on hover var outer = this.$signatureBox, inner = $('.inner', this.$signatureBox).get(0), @@ -181,14 +184,14 @@ RedactorPlugins.signature = { url += 'dept/' + $el.data('deptId'); else if (selected == 'dept' && $el.data('deptField')) { if (dept) - url += 'dept/' + dept + url += 'dept/' + dept; else return inner.empty().parent().hide(); } else if (type == 'none') return inner.empty().parent().hide(); else - url += selected + url += selected; inner.load(url).parent().show(); } @@ -206,10 +209,6 @@ $(function() { .attr('height',img.clientHeight); html = html.replace(before, img.outerHTML); }); - // Drop <inline> elements if found in the text (shady mojo happening - // inside the Redactor editor) - // DELME: When this is fixed upstream in Redactor - html = html.replace(/<inline /, '<span ').replace(/<\/inline>/, '</span>'); return html; }, redact = $.redact = function(el, options) { diff --git a/scp/ajax.php b/scp/ajax.php index 1168513601eb45882783c339934006856b36c550..09dce15dd85103fadcc55bb0b2b25c0a7cc08aac 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -155,6 +155,7 @@ $dispatcher = patterns('', url_post('^(?P<id>\d+)$', 'updateDraft'), url_delete('^(?P<id>\d+)$', 'deleteDraft'), url_post('^(?P<id>\d+)/attach$', 'uploadInlineImage'), + url_post('^(?P<namespace>[\w.]+)/attach$', 'uploadInlineImageEarly'), url_get('^(?P<namespace>[\w.]+)$', 'getDraft'), url_post('^(?P<namespace>[\w.]+)$', 'createDraft'), url_get('^images/browse$', 'getFileList') diff --git a/scp/emailtest.php b/scp/emailtest.php index c714ef1d0643d6c480aac316b511c851244754ef..1246dff088799736f62941acf470f2901b335e1c 100644 --- a/scp/emailtest.php +++ b/scp/emailtest.php @@ -116,8 +116,10 @@ require(STAFFINC_DIR.'header.inc.php'); <div style="padding-top:0.5em;padding-bottom:0.5em"> <em><strong><?php echo __('Message');?></strong>: <?php echo __('email message to send.');?></em> <span class="error">* <?php echo $errors['message']; ?></span></div> <textarea class="richtext draft draft-delete" name="message" cols="21" - data-draft-namespace="email.diag" - rows="10" style="width: 90%;"><?php echo $info['message']; ?></textarea> + rows="10" style="width: 90%;" <?php + list($draft, $attrs) = Draft::getDraftAndDataAttrs('email.diag', false, $info['message']); + echo $attrs; ?>><?php echo $draft ?: $info['message']; + ?></textarea> </td> </tr> </tbody>