1<?php
2/**
3 * Copyright 2011-2017 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file COPYING for license information (GPL). If you
6 * did not receive this file, see http://www.horde.org/licenses/gpl.
7 *
8 * @category  Horde
9 * @copyright 2011-2017 Horde LLC
10 * @license   http://www.horde.org/licenses/gpl GPL
11 * @package   IMP
12 */
13
14/**
15 * Defines an AJAX variable queue for IMP.  These are variables that may be
16 * generated by various IMP code that should be added to the eventual output
17 * sent to the browser.
18 *
19 * @author    Michael Slusarz <slusarz@horde.org>
20 * @category  Horde
21 * @copyright 2011-2017 Horde LLC
22 * @license   http://www.horde.org/licenses/gpl GPL
23 * @package   IMP
24 */
25class IMP_Ajax_Queue
26{
27    /**
28     * Callback method to use for each ftree element.
29     *
30     * @var callback
31     */
32    public $ftreeCallback;
33
34    /**
35     * The list of compose autocompleter address error data.
36     *
37     * @var array
38     */
39    protected $_addr = array();
40
41    /**
42     * The list of attachments.
43     *
44     * @var array
45     */
46    protected $_atc = array();
47
48    /**
49     * The compose object.
50     *
51     * @var IMP_Compose
52     */
53    protected $_compose;
54
55    /**
56     * Flag entries to add to response.
57     *
58     * @var array
59     */
60    protected $_flag = array();
61
62    /**
63     * Add flag configuration to response.
64     *
65     * @var integer
66     */
67    protected $_flagconfig = 0;
68
69    /**
70     * Mailbox options.
71     *
72     * @var array
73     */
74    protected $_mailboxOpts = array();
75
76    /**
77     * Message queue.
78     *
79     * @var array
80     */
81    protected $_messages = array();
82
83    /**
84     * Maillog queue.
85     *
86     * @var array
87     */
88    protected $_maillog = array();
89
90    /**
91     * Poll mailboxes.
92     *
93     * If null, don't output polled information unless explicitly told to.
94     *
95     * @var array
96     */
97    protected $_poll = array();
98
99    /**
100     * Add quota information to response?
101     *
102     * Array w/2 keys: mailbox, force
103     * If null, never sends quota information.
104     *
105     * @var mixed
106     */
107    protected $_quota = false;
108
109    /**
110     * Generates AJAX response task data from the queue.
111     *
112     * For compose autocomplete address error data (key: 'compose-addr'), an
113     * array with keys as the autocomplete DOM element and the values as
114     * arrays. The value arrays have keys as the autocomplete address ID, and
115     * the value is a space-separated list of classnames to add.
116     *
117     * For compose attachment data (key: 'compose-atc'), an array of objects
118     * with these properties:
119     *   - icon: (string) Data url string containing icon information.
120     *   - name: (string) The attachment name
121     *   - num: (integer) The current attachment number
122     *   - size: (string) The size of the attachment
123     *   - type: (string) The MIME type of the attachment
124     *   - view: (boolean) Link to attachment preivew page
125     *
126     * For compose cacheid data (key: 'compose'), an object with these
127     * properties:
128     *   - atclimit: (integer) If set, no further attachments are allowed.
129     *   - cacheid: (string) Current cache ID of the compose message.
130     *
131     * For flag data (key: 'flag'), an array of objects with these properties:
132     *   - add: (array) The list of flags that were added.
133     *   - buids: (string) Indices of the messages that have changed (IMAP
134     *            sequence string; mboxes are base64url encoded).
135     *   - remove: (array) The list of flags that were removed.
136     *   - replace: (array) Replace the flag list with these flags.
137     *
138     * For flag configuration data (key: 'flag-config'), an array containing
139     * flag data. All flags returned in dynamic mode; only flags labeled below
140     * as [sm] are returned in smartmobile mode:
141     *   - a: (boolean) Indicates a flag that can be *a*ltered.
142     *   - b: (string) Background color [sm].
143     *   - c: (string) CSS class.
144     *   - f: (string) Foreground color [sm].
145     *   - i: (string) CSS icon [sm].
146     *   - id: (string) Flag ID (IMAP flag id).
147     *   - l: (string) Flag label [sm].
148     *   - s: (boolean) Indicates a flag that can be *s*earched for [sm].
149     *   - u: (boolean) Indicates a *u*ser flag.
150     *
151     * For mailbox data (key: 'mailbox'), an array with these keys:
152     *   - a: (array) Mailboxes that were added (base64url encoded).
153     *   - all: (integer) TODO
154     *   - base: (string) TODO
155     *   - c: (array) Mailboxes that were changed (base64url encoded).
156     *   - d: (array) Mailboxes that were deleted (base64url encoded).
157     *   - expand: (integer) Expand subfolders on load.
158     *   - switch: (string) Load this mailbox (base64url encoded).
159     *
160     * For maillog data (key: 'maillog'), an object with these properties:
161     *   - buid: (integer) BUID.
162     *   - log: (array) List of log entries.
163     *   - mbox: (string) Mailbox.
164     *
165     * For message preview data (key: 'message'), an object with these
166     * properties:
167     *   - buid: (integer) BUID.
168     *   - data: (object) Message viewport data.
169     *   - mbox: (string) Mailbox.
170     *
171     * For poll data (key: 'poll'), an array with keys as base64url encoded
172     * mailbox names, values as the number of unseen messages.
173     *
174     * For quota data (key: 'quota'), an array with these keys:
175     *   - m: (string) Quota message.
176     *   - p: (integer) Quota percentage.
177     *
178     * @param IMP_Ajax_Application $ajax  The AJAX object.
179     */
180    public function add(IMP_Ajax_Application $ajax)
181    {
182        global $injector;
183
184        /* Add autocomplete address error information. */
185        if (!empty($this->_addr)) {
186            $ajax->addTask('compose-addr', $this->_addr);
187            $this->_addr = array();
188        }
189
190        /* Add compose attachment information. */
191        if (!empty($this->_atc)) {
192            $ajax->addTask('compose-atc', $this->_atc);
193            $this->_atc = array();
194        }
195
196        /* Add compose information. */
197        if (!is_null($this->_compose)) {
198            $compose = new stdClass;
199            if (!$this->_compose->additionalAttachmentsAllowed()) {
200                $compose->atclimit = 1;
201            }
202            $compose->cacheid = $this->_compose->getCacheId();
203            $compose->hmac = $this->_compose->getHmac();
204
205            $ajax->addTask('compose', $compose);
206            $this->_compose = null;
207        }
208
209        /* Add flag information. */
210        if (!empty($this->_flag)) {
211            $ajax->addTask('flag', array_unique($this->_flag, SORT_REGULAR));
212            $this->_flag = array();
213        }
214
215        /* Add flag configuration. */
216        switch ($this->_flagconfig) {
217        case Horde_Registry::VIEW_DYNAMIC:
218        case Horde_Registry::VIEW_SMARTMOBILE:
219            $flags = array();
220            foreach ($injector->getInstance('IMP_Flags')->getList() as $val) {
221                $tmp = array(
222                    'b' => $val->bgdefault ? null : $val->bgcolor,
223                    'f' => $val->fgcolor,
224                    'id' => $val->id,
225                    'l' => $val->label,
226                    's' => intval($val instanceof IMP_Flag_Imap)
227                );
228
229                if ($this->_flagconfig === Horde_Registry::VIEW_DYNAMIC) {
230                    $tmp += array(
231                        'a' => $val->canset,
232                        'c' => $val->css,
233                        'i' => $val->css ? null : $val->cssicon,
234                        'u' => intval($val instanceof IMP_Flag_User)
235                    );
236                }
237
238                $flags[] = array_filter($tmp);
239            }
240            $ajax->addTask('flag-config', $flags);
241            break;
242        }
243
244        /* Add folder tree information. */
245        $this->_addFtreeInfo($ajax);
246
247        /* Add maillog information. */
248        $this->_addMaillogInfo($ajax);
249
250        /* Add message information. */
251        if (!empty($this->_messages)) {
252            $ajax->addTask('message', $this->_messages);
253            $this->_messages = array();
254        }
255
256        /* Add poll information. */
257        $poll = $poll_list = array();
258        if (!empty($this->_poll)) {
259            foreach ($this->_poll as $val) {
260                $poll_list[strval($val)] = 1;
261            }
262        }
263
264        if (count($poll_list)) {
265            $imap_ob = $injector->getInstance('IMP_Factory_Imap')->create();
266            if ($imap_ob->init) {
267                try {
268                    foreach ($imap_ob->status(array_keys($poll_list), Horde_Imap_Client::STATUS_UNSEEN) as $key => $val) {
269                        $poll[IMP_Mailbox::formTo($key)] = intval($val['unseen']);
270                    }
271                } catch (Exception $e) {
272                    // Ignore errors in status() calls.
273                }
274            }
275
276            if (!empty($poll)) {
277                $ajax->addTask('poll', $poll);
278                $this->_poll = array();
279            }
280        }
281
282        /* Add quota information. */
283        if ($this->_quota &&
284            ($quotadata = $injector->getInstance('IMP_Quota_Ui')->quota($this->_quota[0], $this->_quota[1]))) {
285            $ajax->addTask('quota', array(
286                'm' => $quotadata['message'],
287                'p' => round($quotadata['percent']),
288                'l' => $quotadata['percent'] >= 90
289                    ? 'alert'
290                    : ($quotadata['percent'] >= 75 ? 'warn' : '')
291            ));
292            $this->_quota = false;
293        }
294    }
295
296    /**
297     * Return information about the current attachment(s) for a message.
298     *
299     * @param mixed $ob      If an IMP_Compose object, return info on all
300     *                       attachments. If an IMP_Compose_Attachment object,
301     *                       only return information on that object.
302     * @param integer $type  The compose type.
303     */
304    public function attachment($ob, $type = IMP_Compose::COMPOSE)
305    {
306        global $injector;
307
308        $parts = ($ob instanceof IMP_Compose)
309            ? iterator_to_array($ob)
310            : array($ob);
311        $viewer = $injector->getInstance('IMP_Factory_MimeViewer');
312
313        foreach ($parts as $val) {
314            $mime = $val->getPart();
315            $mtype = $mime->getType();
316
317            $tmp = array(
318                'icon' => strval(Horde_Url_Data::create('image/png', file_get_contents($viewer->getIcon($mtype)->fs))),
319                'name' => $mime->getName(true),
320                'num' => $val->id,
321                'type' => $mtype,
322                'size' => IMP::sizeFormat($mime->getBytes())
323            );
324
325            if ($viewer->create($mime)->canRender('full')) {
326                $tmp['url'] = strval($val->viewUrl()->setRaw(true));
327                $tmp['view'] = intval(!in_array($type, array(IMP_Compose::FORWARD_ATTACH, IMP_Compose::FORWARD_BOTH)) && ($mtype != 'application/octet-stream'));
328            }
329
330            $this->_atc[] = $tmp;
331        }
332    }
333
334    /**
335     * Add compose data to the output.
336     *
337     * @param IMP_Compose $ob  The compose object.
338     */
339    public function compose(IMP_Compose $ob)
340    {
341        $this->_compose = $ob;
342    }
343
344    /**
345     * Add address autocomplete error info.
346     *
347     * @param string $domid   The autocomplete DOM ID.
348     * @param string $itemid  The autocomplete address ID.
349     * @param string $class   The classname to add to the address entry.
350     */
351    public function compose_addr($domid, $itemid, $class)
352    {
353        $this->_addr[$domid][$itemid] = $class;
354    }
355
356    /**
357     * Add flag entry to response queue.
358     *
359     * @param array $flags          List of flags that have changed.
360     * @param boolean $add          Were the flags added?
361     * @param IMP_Indices $indices  Indices object.
362     */
363    public function flag($flags, $add, IMP_Indices $indices)
364    {
365        global $injector;
366
367        if ($indices instanceof IMP_Indices_Mailbox) {
368            if (!count($indices) || !$indices->mailbox->access_flags) {
369                return;
370            }
371            $indices = clone $indices;
372            $indices->add($indices->buids);
373        }
374
375        $changed = $injector->getInstance('IMP_Flags')->changed($flags, $add);
376
377        $result = new stdClass;
378        if (!empty($changed['add'])) {
379            $result->add = array_map('strval', $changed['add']);
380        }
381        if (!empty($changed['remove'])) {
382            $result->remove = array_map('strval', $changed['remove']);
383        }
384
385        $result->buids = $indices->toArray();
386        $this->_flag[] = $result;
387    }
388
389    /**
390     * Sends replacement flag information for the indices provided.
391     *
392     * @param IMP_Indices $indices  Indices object.
393     */
394    public function flagReplace(IMP_Indices $indices)
395    {
396        global $injector, $prefs;
397
398        $imp_flags = $injector->getInstance('IMP_Flags');
399
400        foreach ($indices as $ob) {
401            $list_ob = $ob->mbox->list_ob;
402            $msgnum = array();
403
404            foreach ($ob->uids as $uid) {
405                $msgnum[] = $list_ob->getArrayIndex($uid) + 1;
406            }
407
408            $marray = $list_ob->getMailboxArray($msgnum, array(
409                'headers' => true,
410                'type' => $prefs->getValue('atc_flag')
411            ));
412
413            foreach ($marray['overview'] as $val) {
414                $result = new stdClass;
415                $result->buids = $ob->mbox->toBuids(new IMP_Indices($ob->mbox, $val['uid']))->toArray();
416                $result->replace = array_map('strval', $imp_flags->parse(array(
417                    'flags' => $val['flags'],
418                    'headers' => $val['headers'],
419                    'runhook' => $val,
420                    'personal' => $val['envelope']->to
421                )));
422                $this->_flag[] = $result;
423            }
424        }
425    }
426
427    /**
428     * Add flag configuration information to response queue.
429     *
430     * @param integer $view  The current view.
431     */
432    public function flagConfig($view)
433    {
434        $this->_flagconfig = $view;
435    }
436
437    /**
438     * Add message data to output.
439     *
440     * @param IMP_Indices $indices  Index of the message.
441     * @param boolean $preview      Preview data?
442     * @param boolean $peek         Don't set seen flag?
443     */
444    public function message(IMP_Indices $indices, $preview = false,
445                            $peek = false)
446    {
447        try {
448            $show_msg = new IMP_Ajax_Application_ShowMessage($indices, $peek);
449            $msg = (object)$show_msg->showMessage(array(
450                'preview' => $preview
451            ));
452            $msg->save_as = strval($msg->save_as);
453
454            if ($indices instanceof IMP_Indices_Mailbox) {
455                $indices = $indices->buids;
456            }
457
458            foreach ($indices as $val) {
459                foreach ($val->uids as $val2) {
460                    $ob = new stdClass;
461                    $ob->buid = $val2;
462                    $ob->data = $msg;
463                    $ob->mbox = $val->mbox->form_to;
464                    $this->_messages[] = $ob;
465                }
466            }
467        } catch (Exception $e) {}
468    }
469
470    /**
471     * Add maillog data to output.
472     *
473     * @param IMP_Indices $indices  Indices object.
474     */
475    public function maillog(IMP_Indices $indices)
476    {
477        $this->_maillog[] = $indices;
478    }
479
480    /**
481     * Add additional options to the mailbox output.
482     *
483     * @param array $name   Option name.
484     * @param mixed $value  Option value.
485     */
486    public function setMailboxOpt($name, $value)
487    {
488        $this->_mailboxOpts[$name] = $value;
489    }
490
491    /**
492     * Add poll entry to response queue.
493     *
494     * @param mixed $mboxes      A mailbox name or list of mailbox names.
495     * @param boolean $explicit  If true, explicitly output poll information.
496     *                           Otherwise, add only if not disabled.
497     */
498    public function poll($mboxes, $explicit = false)
499    {
500        if (is_null($this->_poll)) {
501            if (!$explicit) {
502                return;
503            }
504            $this->_poll = array();
505        } elseif (empty($this->_poll) && is_null($mboxes)) {
506            $this->_poll = null;
507            return;
508        }
509
510        if (!is_array($mboxes)) {
511            $mboxes = array($mboxes);
512        }
513
514        foreach (IMP_Mailbox::get($mboxes) as $val) {
515            if ($val->polled) {
516                $this->_poll[] = $val;
517            }
518        }
519    }
520
521    /**
522     * Add quota entry to response queue.
523     *
524     * @param string|null $mailbox  Mailbox to query for quota. If null,
525     *                              disables quota output.
526     * @param boolean $force        If true, force output. If false, output
527     *                              based on configured quota interval.
528     */
529    public function quota($mailbox, $force = true)
530    {
531        if (!is_null($this->_quota)) {
532            if (is_null($mailbox)) {
533                /* Disable quota output entirely. */
534                $this->_quota = null;
535            } elseif (!is_array($this->_quota) || !$this->_quota[1]) {
536                /* Don't change a previously issued force quota request. */
537                $this->_quota = array($mailbox, $force);
538            }
539        }
540    }
541
542    /**
543     * Add folder tree information.
544     *
545     * @param IMP_Ajax_Application $ajax  The AJAX object.
546     */
547    protected function _addFtreeInfo(IMP_Ajax_Application $ajax)
548    {
549        global $injector;
550
551        $eltdiff = $injector->getInstance('IMP_Ftree')->eltdiff;
552        $out = $poll = array();
553
554        if (!$eltdiff->track) {
555            return;
556        }
557
558        if (($add = $eltdiff->add) &&
559            ($elts = array_values(array_filter(array_map(array($this, '_ftreeElt'), $add))))) {
560            $out['a'] = $elts;
561            $poll = $add;
562        }
563
564        if (($change = $eltdiff->change) &&
565            ($elts = array_values(array_filter(array_map(array($this, '_ftreeElt'), $change))))) {
566            $out['c'] = $elts;
567            $poll = array_merge($poll, $change);
568        }
569
570        if ($delete = $eltdiff->delete) {
571            $out['d'] = IMP_Mailbox::formTo($delete);
572        }
573
574        if (!empty($out)) {
575            $eltdiff->clear();
576            $ajax->addTask('mailbox', array_merge($out, $this->_mailboxOpts));
577            $this->poll($poll);
578        }
579    }
580
581    /**
582     * Create a folder tree element.
583     *
584     * @param string $id  Tree element ID.
585     *
586     * @return mixed  The element object, or null if the element is not
587     *                active. Object contains the following properties:
588     * <pre>
589     *   - ch: (boolean) [children] Does the mailbox contain children?
590     *         DEFAULT: no
591     *   - cl: (string) [class] The CSS class.
592     *         DEFAULT: 'folderImg'
593     *   - co: (boolean) [container] Is this mailbox a container element?
594     *         DEFAULT: no
595     *   - fs: (boolean) [boolean] Fixed element for sorting purposes.
596     *         DEFAULT: no
597     *   - i: (string) [icon] A user defined icon to use.
598     *        DEFAULT: none
599     *   - l: (string) [label] The mailbox display label.
600     *   - m: (string) [mbox] The mailbox value (base64url encoded).
601     *   - n: (boolean) [non-imap] A non-IMAP element?
602     *        DEFAULT: no
603     *   - nc: (boolean) [no children] Does the element not allow children?
604     *         DEFAULT: no
605     *   - ns: (boolean) [no sort] Don't sort on browser.
606     *         DEFAULT: no
607     *   - pa: (string) [parent] The parent element.
608     *         DEFAULT: DimpCore.conf.base_mbox
609     *   - po: (boolean) [polled] Is the element polled?
610     *         DEFAULT: no
611     *   - r: (integer) [remote] Is this a "remote" element? 1 is the remote
612     *        container, 2 is a remote account, and 3 is a remote mailbox.
613     *        DEFAULT: 0
614     *   - s: (boolean) [special] Is this a "special" element?
615     *        DEFAULT: no
616     *   - t: (string) [title] Mailbox title.
617     *        DEFAULT: 'l' val
618     *   - un: (boolean) [unsubscribed] Is this mailbox unsubscribed?
619     *         DEFAULT: no
620     *   - v: (integer) [virtual] Virtual folder? 0 = not vfolder, 1 = system
621     *        vfolder, 2 = user vfolder
622     *        DEFAULT: 0
623     *  </pre>
624     */
625    protected function _ftreeElt($id)
626    {
627        global $injector;
628
629        $ftree = $injector->getInstance('IMP_Ftree');
630        if (!($elt = $ftree[$id]) || $elt->base_elt) {
631            return null;
632        }
633
634        $mbox_ob = $elt->mbox_ob;
635
636        $ob = new stdClass;
637        $ob->m = $mbox_ob->form_to;
638
639        if ($elt->children) {
640            $ob->ch = 1;
641        } elseif ($elt->nochildren) {
642            $ob->nc = 1;
643        }
644
645        $ob->l = htmlspecialchars($mbox_ob->abbrev_label);
646        $label = $mbox_ob->display_notranslate;
647        if ($ob->l != $label) {
648            $ob->t = $label;
649        }
650
651        $parent = $elt->parent;
652        if (!$parent->base_elt) {
653            $ob->pa = $parent->mbox_ob->form_to;
654            if ($parent->remote &&
655                (strcasecmp($mbox_ob->imap_mbox, 'INBOX') === 0)) {
656                $ob->fs = 1;
657            }
658        }
659
660        if ($elt->vfolder) {
661            $ob->v = $mbox_ob->editvfolder ? 2 : 1;
662            $ob->ns = 1;
663        }
664
665        if ($elt->nonimap) {
666            $ob->n = 1;
667            if ($mbox_ob->remote_container) {
668                $ob->r = 1;
669            }
670        }
671
672        if ($elt->container) {
673            if (empty($ob->ch)) {
674                return null;
675            }
676            $ob->co = 1;
677        } else {
678            if (!$elt->subscribed) {
679                $ob->un = 1;
680            }
681
682            if (isset($ob->n) && isset($ob->r)) {
683                $ob->r = ($mbox_ob->remote_account->imp_imap->init ? 3 : 2);
684            }
685
686            if ($elt->polled) {
687                $ob->po = 1;
688            }
689
690            if ($elt->inbox) {
691                $ob->ns = $ob->s = 1;
692            } elseif ($mbox_ob->special) {
693                $ob->ns = $ob->s = 1;
694                $ob->t = strval($elt);
695            }
696        }
697
698        $icon = $mbox_ob->icon;
699        if ($icon->user_icon) {
700            $ob->cl = 'customimg';
701            $ob->i = strval($icon->icon);
702        } elseif (!in_array($icon->class, array('folderImg', 'folderopenImg'))) {
703            $ob->cl = $icon->class;
704        }
705
706        if ($this->ftreeCallback) {
707            call_user_func($this->ftreeCallback, $id, $ob);
708        }
709
710        return $ob;
711    }
712
713    /**
714     * Add maillog information.
715     *
716     * @param IMP_Ajax_Application $ajax  The AJAX object.
717     */
718    protected function _addMaillogInfo(IMP_Ajax_Application $ajax)
719    {
720        global $injector;
721
722        if (empty($this->_maillog)) {
723            return;
724        }
725
726        $imp_maillog = $injector->getInstance('IMP_Maillog');
727        $maillog = array();
728
729        foreach ($this->_maillog as $val) {
730            /* Need to grab the maillog data from the "real" ID. Then check to
731             * see if a different BUID exists, since the message log may need
732             * to be updated in two locations at once (i.e. search mailbox and
733             * real mailbox). */
734            foreach ($val as $v) {
735                foreach ($v->uids as $v2) {
736                    $msg = new IMP_Maillog_Message(
737                        new IMP_Indices($v->mbox, $v2)
738                    );
739
740                    $tmp = array();
741                    foreach ($imp_maillog->getLog($msg, array('mdn')) as $v3) {
742                        $tmp[] = array(
743                            'm' => $v3->message,
744                            't' => $v3->action
745                        );
746                    }
747
748                    if ($tmp) {
749                        $indices = $msg->indices;
750                        if ($val instanceof IMP_Indices_Mailbox) {
751                            $indices->add($val->mailbox->toBuids($indices));
752                        }
753
754                        foreach ($indices as $v4) {
755                            foreach ($v4->uids as $v5) {
756                                $log_ob = new stdClass;
757                                $log_ob->buid = intval($v5);
758                                $log_ob->log = $tmp;
759                                $log_ob->mbox = $v4->mbox->form_to;
760                                $maillog[] = $log_ob;
761                            }
762                        }
763                    }
764                }
765            }
766        }
767
768        if (!empty($maillog)) {
769            $ajax->addTask('maillog', $maillog);
770        }
771    }
772
773}
774