diff --git a/include/ajax.draft.php b/include/ajax.draft.php index f727e2cc9a10040786f6abb3502a8a21f7e04806..c50817dac4598a0a4c4ce42b6585f4af61cd5566 100644 --- a/include/ajax.draft.php +++ b/include/ajax.draft.php @@ -7,29 +7,24 @@ 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'])) { + $field_list = array('response', 'note', 'answer', 'body', + 'message', 'issue'); + foreach ($field_list as $field) { + if (isset($_POST[$field])) { + $vars['body'] = $_POST[$field]; + break; + } } } - if (!isset($vars['body'])) - return Http::response(422, "Draft body not found in request"); + if (!isset($vars['body']) || !$vars['body']) + return JsonDataEncoder::encode(array( + 'error' => __("Draft body not found in request"), + 'code' => 422, + )); - $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 (!($draft = Draft::create($vars)) || !$draft->save()) + Http::response(500, 'Unable to create draft'); echo JsonDataEncoder::encode(array( 'draft_id' => $draft->getId(), @@ -232,6 +227,9 @@ class DraftAjaxAPI extends AjaxController { 'namespace' => $namespace, ); + if (isset($_POST['name'])) + $vars['body'] = $_POST[$_POST['name']]; + return self::_createDraft($vars); } diff --git a/include/class.draft.php b/include/class.draft.php index cdb5d43f440081d3e047993dd0df006cf23c39d6..a58fb52f95828305495d89b7e0ef8e36f0dc401b 100644 --- a/include/class.draft.php +++ b/include/class.draft.php @@ -1,28 +1,57 @@ <?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"', $namespace)); + $criteria = array('namespace'=>$namespace); + if ($id) { + $attrs[] = sprintf('data-draft-object-id="%s"', $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($original)); + + return array($draft_body, implode(' ', $attrs)); + } function getAttachmentIds($body=false) { $attachments = array(); @@ -63,69 +92,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 $this->namespace && isset($this->body) && isset($this->staff_id); + } + + function save($refetch=false) { + if (!$this->isValid()) return false; - $sql = ' SET `namespace`='.db_input($vars['namespace']) - .' ,body='.db_input(Format::sanitize($vars['body'], false)) - .' ,staff_id='.db_input($vars['staff_id']); + return parent::save($refetch); + } - if (!$id) { - $sql = 'INSERT INTO '.DRAFT_TABLE.$sql - .' ,created=NOW()'; - if(!db_query($sql) || !($draft=self::lookup(db_insert_id()))) - return false; + static function create($vars) { + $attachments = @$vars['attachments']; + unset($vars['attachments']); - // Cloned attachments... - if($vars['attachments'] && is_array($vars['attachments'])) - $draft->attachments->upload($vars['attachments'], true); + $vars['created'] = SqlFunction::NOW(); + $draft = parent::create($vars); - return $draft; - } - else { - $sql = 'UPDATE '.DRAFT_TABLE.$sql - .' WHERE id='.db_input($id); - if (db_query($sql) && db_affected_rows() == 1) - return $this; - } - } - - 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 + )); } /** diff --git a/js/redactor-osticket.js b/js/redactor-osticket.js index cded2e1f3ffdc7d425428155eec3cf2a0815777d..9bd0e57d7f844ce3b643b6c85d8a88191effb37a 100644 --- a/js/redactor-osticket.js +++ b/js/redactor-osticket.js @@ -21,10 +21,17 @@ 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.autosave = this.opts.autoCreateUrl = autosave_url; this.opts.autosaveInterval = 10; - this.opts.autosaveCallback = this.setupDraftUpdate; - this.opts.initCallback = this.recoverDraft; + this.opts.autosaveCallback = this.afterUpdateDraft; + this.opts.autosaveErrorCallback = this.autosaveFailed; + 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'; + } + this.opts.imageUploadErrorCallback = this.displayError; this.$draft_saved = $('<span>') .addClass("pull-right draft-saved") @@ -33,73 +40,55 @@ 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 the draft was created, a draft_id will be sent back — update + // the URL to send updates in the future + if (data.draft_id) { + this.opts.draftId = data.draft_id; + this.opts.autosave = 'ajax.php/draft/' + data.draft_id; + } - if (!data || !data.draft_id) + // Only show the [Draft Saved] notice if there is content in the + // field that has been touched + if (this.opts.draftOriginal && this.opts.draftOriginal == this.get()) { + // No change yet — dont't show the button + return; + } + if (data) { + 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 +100,19 @@ 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.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(); } }); } @@ -141,10 +131,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 +171,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 +196,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) {