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