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