1<?php 2 3/** 4 * Class: Draft 5 * 6 * Defines a simple draft-saving mechanism for osTicket which supports draft 7 * fetch and update via an ajax mechanism (include/ajax.draft.php). 8 * 9 * Fields: 10 * id - (int:auto:pk) Draft ID number 11 * body - (text) Body of the draft 12 * namespace - (string) Identifier of draft grouping — useful for multiple 13 * drafts on the same document by different users 14 * staff_id - (int:null) Staff owner of the draft 15 * extra - (text:json) Extra attributes of the draft 16 * created - (date) Date draft was initially created 17 * updated - (date:null) Date draft was last updated 18 */ 19class Draft extends VerySimpleModel { 20 21 static $meta = array( 22 'table' => DRAFT_TABLE, 23 'pk' => array('id'), 24 'joins' => array( 25 'attachments' => array( 26 'constraint' => array( 27 "'D'" => 'Attachment.type', 28 'id' => 'Attachment.object_id', 29 ), 30 'list' => true, 31 'null' => true, 32 'broker' => 'GenericAttachments', 33 ), 34 ), 35 ); 36 37 function getId() { return $this->id; } 38 function getBody() { return $this->body; } 39 function getStaffId() { return $this->staff_id; } 40 function getNamespace() { return $this->namespace; } 41 42 static protected function getCurrentUserId() { 43 global $thisstaff, $thisclient; 44 45 $user = $thisstaff ?: $thisclient; 46 if ($user) 47 return $user->getId(); 48 49 return 1 << 31; 50 } 51 52 static function getDraftAndDataAttrs($namespace, $id=0, $original='') { 53 $draft_body = null; 54 $attrs = array(sprintf('data-draft-namespace="%s"', Format::htmlchars($namespace))); 55 $criteria = array( 56 'namespace' => $namespace, 57 'staff_id' => self::getCurrentUserId(), 58 ); 59 if ($id) { 60 $attrs[] = sprintf('data-draft-object-id="%s"', Format::htmlchars($id)); 61 $criteria['namespace'] .= '.' . $id; 62 } 63 if ($draft = static::objects()->filter($criteria)->first()) { 64 $attrs[] = sprintf('data-draft-id="%s"', $draft->getId()); 65 $draft_body = $draft->getBody(); 66 } 67 $attrs[] = sprintf('data-draft-original="%s"', 68 Format::htmlchars(Format::viewableImages($original))); 69 70 return array(Format::htmlchars(Format::viewableImages($draft_body)), 71 implode(' ', $attrs)); 72 } 73 74 function getAttachmentIds($body=false) { 75 $attachments = array(); 76 if (!$body) 77 $body = $this->getBody(); 78 $body = Format::localizeInlineImages($body); 79 $matches = array(); 80 if (preg_match_all('/"cid:([\\w.-]{32})"/', $body, $matches)) { 81 $files = AttachmentFile::objects() 82 ->filter(array('key__in' => $matches[1])); 83 foreach ($files as $F) { 84 $attachments[] = array( 85 'id' => $F->getId(), 86 'inline' => true 87 ); 88 } 89 } 90 return $attachments; 91 } 92 93 /* 94 * Ensures that the inline attachments cited in the body of this draft 95 * are also listed in the draft_attachment table. After calling this, 96 * the ::getAttachments() function should correctly return all inline 97 * attachments. This function should be called after creating a draft 98 * with an existing body 99 */ 100 function syncExistingAttachments() { 101 $matches = array(); 102 if (!preg_match_all('/"cid:([\\w.-]{32})"/', $this->getBody(), $matches)) 103 return; 104 105 // Purge current attachments 106 $this->attachments->deleteInlines(); 107 foreach (AttachmentFile::objects() 108 ->filter(array('key__in' => $matches[1])) 109 as $F 110 ) { 111 $this->attachments->upload($F->getId(), true); 112 } 113 } 114 115 function setBody($body) { 116 // Change file.php urls back to content-id's 117 $body = Format::sanitize($body, false, 118 // Preserve annotation information, if any 119 'img=data-annotations,data-orig-annotated-image-src'); 120 121 $this->body = $body ?: ' '; 122 $this->updated = SqlFunction::NOW(); 123 return $this->save(); 124 } 125 126 function delete() { 127 $this->attachments->deleteAll(); 128 return parent::delete(); 129 } 130 131 function isValid() { 132 // Required fields 133 return $this->namespace && isset($this->staff_id); 134 } 135 136 function save($refetch=false) { 137 if (!$this->isValid()) 138 return false; 139 140 return parent::save($refetch); 141 } 142 143 static function create($vars=false) { 144 $attachments = @$vars['attachments']; 145 unset($vars['attachments']); 146 147 $vars['created'] = SqlFunction::NOW(); 148 $vars['staff_id'] = self::getCurrentUserId(); 149 $draft = new static($vars); 150 151 // Cloned attachments ... 152 if (false && $attachments && is_array($attachments)) 153 // XXX: This won't work until the draft is saved 154 $draft->attachments->upload($attachments, true); 155 156 return $draft; 157 } 158 159 static function lookupByNamespaceAndStaff($namespace, $staff_id) { 160 return static::lookup(array( 161 'namespace'=>$namespace, 162 'staff_id'=>$staff_id 163 )); 164 } 165 166 /** 167 * Delete drafts saved for a particular namespace. If the staff_id is 168 * specified, only drafts owned by that staff are deleted. Usually, if 169 * closing a ticket, the staff_id should be left null so that all drafts 170 * are cleaned up. 171 */ 172 static function deleteForNamespace($namespace, $staff_id=false) { 173 $attachments = Attachment::objects() 174 ->filter(array('draft__namespace__startswith' => $namespace)); 175 if ($staff_id) 176 $attachments->filter(array('draft__staff_id' => $staff_id)); 177 178 $attachments->delete(); 179 180 $criteria = array('namespace__like'=>$namespace); 181 if ($staff_id) 182 $criteria['staff_id'] = $staff_id; 183 return static::objects()->filter($criteria)->delete(); 184 } 185 186 static function cleanup() { 187 // Keep drafts for two weeks (14 days) 188 $sql = 'DELETE FROM '.DRAFT_TABLE 189 ." WHERE (updated IS NULL AND datediff(now(), created) > 14) 190 OR datediff(now(), updated) > 14"; 191 return db_query($sql); 192 } 193} 194 195?> 196