comparison plugins/advanced_search/advanced_search.php~ @ 34:50ac5484d514

one fix to distro
author Charlie Root
date Sun, 27 May 2018 16:53:56 -0400
parents
children
comparison
equal deleted inserted replaced
33:d41c01c5c933 34:50ac5484d514
1 <?php
2 /**
3 * Processing an advanced search over an E-Mail Account
4 *
5 * @version 2.1.5
6 * @licence GNU GPLv3+
7 * @author Wilwert Claude
8 * @author Ludovicy Steve
9 * @author Chris Moules
10 * @website http://www.gms.lu
11 *
12 * Updated by Alex S Grebenschikov (www.poralix.com)
13 * to make it compatible with RoundCube 1.3.0
14 * Last modified: Tue Aug 1 06:41:46 CEST 2017
15 *
16 */
17
18 class advanced_search extends rcube_plugin
19 {
20
21 /**
22 * Instance of rcmail
23 *
24 * @var object
25 * @access private
26 */
27 private $rc;
28
29 public $task = 'mail|settings';
30
31 /**
32 * Plugin config
33 *
34 * @var array
35 * @access private
36 */
37 private $config;
38
39 /**
40 * Localization strings
41 *
42 * @var array
43 * @access private
44 */
45 private $i18n_strings = array();
46
47 /**
48 * Initialisation of the plugin
49 *
50 * @access public
51 * @return null
52 */
53 public function init()
54 {
55 $this->rc = rcmail::get_instance();
56 $this->load_config("config-default.inc.php");
57 $this->load_config();
58 $this->config = $this->rc->config->get('advanced_search_plugin');
59 $this->register_action('plugin.display_advanced_search', array($this, 'display_advanced_search'));
60 $this->register_action('plugin.trigger_search', array($this, 'trigger_search'));
61 $this->register_action('plugin.trigger_search_pagination', array($this, 'trigger_search_pagination'));
62 $this->register_action('plugin.save_search', array($this, 'save_search'));
63 $this->register_action('plugin.delete_search', array($this, 'delete_search'));
64 $this->register_action('plugin.get_saved_search', array($this, 'get_saved_search'));
65
66 $this->skin = $this->rc->config->get('skin');
67 $this->add_texts('localization', true);
68 $this->populate_i18n();
69 if(isset($this->config['criteria'])) {
70 foreach($this->config['criteria'] as $key => $translation) {
71 $this->config['criteria'][$key] = $this->gettext($key);
72 }
73 }
74 $this->include_script('advanced_search.js');
75
76 if ($this->rc->task == 'mail') {
77 $file = 'skins/' . $this->skin . '/advanced_search.css';
78
79 if (file_exists($this->home . '/' . $file)) {
80 $this->include_stylesheet($file);
81 } else {
82 $this->include_stylesheet('skins/default/advanced_search.css');
83 }
84
85 if (empty($this->rc->action)) {
86 $this->add_menu_entry();
87 }
88 } elseif ($this->rc->task == 'settings') {
89 $this->add_hook('preferences_list', array($this, 'preferences_list'));
90 $this->add_hook('preferences_save', array($this, 'preferences_save'));
91 $this->add_hook('preferences_sections_list', array($this, 'preferences_section'));
92 $file = 'skins/' . $this->skin . '/advanced_search.css';
93 if (file_exists($this->home . '/' . $file)) {
94 $this->include_stylesheet($file);
95 } else {
96 $this->include_stylesheet('skins/default/advanced_search.css');
97 }
98 }
99
100 $this->add_hook('startup', array($this, 'startup'));
101 }
102
103
104 public function startup($args)
105 {
106 $search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GET);
107 if (!isset($search)) {
108 $search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_POST);
109 }
110 $rsearch = $search == 'advanced_search_active';
111 $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GET);
112 $draft_uid = rcube_utils::get_input_value('_draft_uid', rcube_utils::INPUT_GET);
113 $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GET);
114 $page = rcube_utils::get_input_value('_page', rcube_utils::INPUT_GET);
115 $sort = rcube_utils::get_input_value('_sort', rcube_utils::INPUT_GET);
116
117 if (!empty($uid)) {
118 $parts = explode('__MB__', $uid);
119 if (count($parts) == 2) {
120 $search = 'advanced_search_active';
121 }
122 }
123 if (!empty($draft_uid)) {
124 $parts = explode('__MB__', $draft_uid);
125 if (count($parts) == 2) {
126 $search = 'advanced_search_active';
127 }
128 }
129
130 if ($search == 'advanced_search_active') {
131 if ($args['action'] == 'show' && !empty($uid)) {
132 $parts = explode('__MB__', $uid);
133 $uid = $parts[0];
134 $this->rc->output->redirect(array('_task' => 'mail', '_action' => $args['action'], '_mbox' => $mbox, '_uid' => $uid));
135 }
136 if ($args['action'] == 'compose') {
137 $draft_uid = rcube_utils::get_input_value('_draft_uid', rcube_utils::INPUT_GET);
138 $parts = explode('__MB__', $draft_uid);
139 $draft_uid = $parts[0];
140 if (!empty($draft_uid)) {
141 $this->rc->output->redirect(array('_task' => 'mail', '_action' => $args['action'], '_mbox' => $mbox, '_draft_uid' => $draft_uid));
142 }
143 }
144 if ($args['action'] == 'list' && $rsearch) {
145 $this->rc->output->command('advanced_search_active', '_page=' . $page . '&_sort=' . $sort);
146 $this->rc->output->send();
147 $args['abort'] = true;
148 }
149 if ($args['action'] == 'mark') {
150 $flag = rcube_utils::get_input_value('_flag', rcube_utils::INPUT_POST);
151 $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
152
153 $post_str = '_flag=' . $flag . '&_uid=' . $uid;
154 if ($quiet = rcube_utils::get_input_value('_quiet', rcube_utils::INPUT_POST)) {
155 $post_str .= '&_quiet=' . $quiet;
156 }
157 if ($from = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST)) {
158 $post_str .= '&_from=' . $from;
159 }
160 if ($count = rcube_utils::get_input_value('_count', rcube_utils::INPUT_POST)) {
161 $post_str .= '&_count=' . $count;
162 }
163 if ($ruid = rcube_utils::get_input_value('_ruid', rcube_utils::INPUT_POST)) {
164 $post_str .= '&_ruid=' . $ruid;
165 }
166 $this->rc->output->command('label_mark', $post_str);
167 $this->rc->output->send();
168 $args['abort'] = true;
169 }
170
171 } else {
172 if ($args['action'] != 'plugin.get_saved_search' && $args['action'] != 'plugin.save_search' && $args['action'] != 'plugin.delete_search') {
173 $this->rc->output->command('plugin.advanced_search_del_header', array());
174 }
175 }
176 }
177
178 /**
179 * This function populates an array with localization texts.
180 * This is needed as ew are using a lot of localizations from core.
181 * The core localizations are not avalable directly in JS
182 *
183 * @access private
184 * @return null
185 */
186 private function populate_i18n()
187 {
188 $core = array('advsearch', 'search', 'resetsearch', 'addfield', 'delete', 'cancel');
189
190 foreach ($core as $label) {
191 $this->i18n_strings[$label] = $this->rc->gettext($label);
192 }
193
194 $local = array('in', 'and', 'or', 'not', 'where', 'exclude', 'andsubfolders', 'allfolders', 'save_the_search', 'has_been_saved', 'deletesearch', 'has_been_deleted');
195
196 foreach ($local as $label) {
197 $this->i18n_strings[$label] = $this->gettext($label);
198 }
199 }
200
201 /**
202 * This adds a button into the configured menu to use the advanced search
203 *
204 * @access public
205 * @return null
206 */
207 public function add_menu_entry()
208 {
209 $displayOptions = $this->rc->config->get('advanced_search_display_options', array());
210 $target_menu = (isset($displayOptions['target_menu']) && !empty($displayOptions['target_menu'])) ? $displayOptions['target_menu'] : $this->config['target_menu'];
211 if ($target_menu != 'toolbar') {
212 $this->api->add_content(
213 html::tag(
214 'li',
215 null,
216 $this->api->output->button(
217 array(
218 'command' => 'plugin.advanced_search',
219 'label' => 'advsearch',
220 'type' => 'link',
221 'classact' => 'icon advanced-search active',
222 'class' => 'icon advanced-search',
223 'innerclass' => 'icon advanced-search'
224 )
225 )
226 ),
227 $target_menu
228 );
229 } else {
230 $this->api->add_content(
231 $this->api->output->button(
232 array(
233 'command' => 'plugin.advanced_search',
234 'title' => 'advsearch',
235 'label' => 'search',
236 'type' => 'link',
237 'classact' => 'button advanced-search active',
238 'class' => 'button advanced-search',
239 'innerclass' => 'button advanced-search',
240 )
241 ),
242 $target_menu
243 );
244 }
245 }
246
247 /**
248 * This function quotes some specific values based on their data type
249 *
250 * @param mixed $input The value to get quoted
251 * @access public
252 * @return The quoted value
253 */
254 public function quote($value)
255 {
256 if (getType($value) == 'string') {
257 if (!preg_match('/"/', $value)) {
258 $value = preg_replace('/^"/', '', $value);
259 $value = preg_replace('/"$/', '', $value);
260 $value = preg_replace('/"/', '\\"', $value);
261 }
262
263 $value = '"' . $value . '"';
264 }
265
266 return $value;
267 }
268
269 /**
270 * This function generates the IMAP compatible search query based on the request data (by javascript)
271 *
272 * @param array $input The raw criteria data sent by javascript
273 * @access private
274 * @return string or int
275 */
276 private function process_search_part($search_part)
277 {
278 $command_str = '';
279 $flag = false;
280
281 // Check for valid input
282 if (!array_key_exists($search_part['filter'], $this->config['criteria'])) {
283 $this->rc->output->show_message($this->gettext('internalerror'), 'error');
284
285 return 0;
286 }
287 if (in_array($search_part['filter'], $this->config['flag_criteria'])) {
288 $flag = true;
289 }
290 if (!$flag && !(isset($search_part['filter-val']) && $search_part['filter-val'] != '')) {
291 return 1;
292 }
293
294 // Negate part
295 if ($search_part['not'] == 'true') {
296 $command_str .= 'NOT ';
297 }
298
299 $command_str .= $search_part['filter'];
300
301 if (!$flag) {
302 if (in_array($search_part['filter'], $this->config['date_criteria'])) {
303 // Take date format from user environment
304 $date_format = $this->rc->config->get('date_format', 'Y-m-d');
305 // Try to use PHP5.2+ DateTime but fallback to ugly old method
306 if (class_exists('DateTime')) {
307 $date = DateTime::createFromFormat($date_format, $search_part['filter-val']);
308 $command_str .= ' ' . $this->quote($date->format("d-M-Y"));
309 } else {
310 $date_format = preg_replace('/(\w)/', '%$1', $date_format);
311 $date_array = strptime($search_part['filter-val'], $date_format);
312 $unix_ts = mktime($date_array['tm_hour'], $date_array['tm_min'], $date_array['tm_sec'], $date_array['tm_mon']+1, $date_array['tm_mday'], $date_array['tm_year']+1900);
313 $command_str .= ' ' . $this->quote(date("d-M-Y", $unix_ts));
314 }
315 } elseif (in_array($search_part['filter'], $this->config['email_criteria'])) {
316 // Strip possible ',' added by autocomplete
317 $command_str .= ' ' . $this->quote(trim($search_part['filter-val'], " \t,"));
318 } else {
319 // Don't try to use a value for a binary flag object
320 $command_str .= ' ' . $this->quote($search_part['filter-val']);
321 }
322 }
323
324 return $command_str;
325 }
326
327 /**
328 * This function generates the IMAP compatible search query based on the request data (by javascript)
329 *
330 * @param array $input The raw criteria data sent by javascript
331 * @access public
332 * @return The final search command
333 */
334 public function get_search_query($input)
335 {
336 $command = array();
337
338 foreach ($input as $search_part) {
339 // Skip excluded parts
340 if ($search_part['excluded'] == 'true') {
341 continue;
342 }
343 if (! $part_command = $this->process_search_part($search_part)) {
344 return 0;
345 }
346 // Skip invalid parts
347 if ($part_command === 1) {
348 continue;
349 }
350
351 $command[] = array('method' => isset($search_part['method']) ? $search_part['method'] : 'and',
352 'command' => $part_command);
353 }
354
355 $command_string = $this->build_search_string($command);
356
357 return $command_string;
358 }
359
360 /**
361 * This function converts the preconfigured query parts (as array) into an IMAP compatible string
362 *
363 * @param array $command_array An array containing the advanced search criteria
364 * @access public
365 * @return The command string
366 */
367 private function build_search_string($command_array)
368 {
369 $command = array();
370 $paranthesis = 0;
371 $prev_method = null;
372 $next_method = null;
373 $cnt = count($command_array);
374
375 foreach ($command_array as $k => $v) {
376 $part = '';
377 $next_method = 'unknown';
378
379 // Lookup next method
380 if ($k < $cnt-1) {
381 $next_method = $command_array[$k+1]['method'];
382 }
383
384 // If previous option was OR, close any open brakets
385 if ($paranthesis > 0 && $prev_method == 'or' && $v['method'] != 'or') {
386 for (; $paranthesis > 0; $paranthesis--) {
387 $part .= ')';
388 }
389 }
390
391 // If there are two consecutive ORs, add brakets
392 // If the next option is a new OR, add the prefix here
393 // If the next option is _not_ an OR, and the current option is AND, prefix ALL
394 if ($next_method == 'or') {
395 if ($v['method'] == 'or') {
396 $part .= ' (';
397 $paranthesis++;
398 }
399 $part .= 'OR ';
400 } elseif ($v['method'] == 'and') {
401 $part .= 'ALL ';
402 }
403
404 $part .= $v['command'];
405
406 // If this is the end of the query, and we have open brakets, close them
407 if ($k == $cnt-1 && $paranthesis > 0) {
408 for (; $paranthesis > 0; $paranthesis--) {
409 $part .= ')';
410 }
411 }
412
413 $prev_method = $v['method'];
414 $command[] = $part;
415 }
416
417 $command = implode(' ', $command);
418
419 return $command;
420 }
421
422 /**
423 * This functions sends the initial data to the client side where a form (in dialog) is built for the advanced search
424 *
425 * @access public
426 * @return null
427 */
428 public function display_advanced_search()
429 {
430 $ret = array('html' => $this->generate_searchbox(),
431 'row' => $this->add_row(),
432 'saved_searches' => $this->get_saved_search_names(),
433 'title' => $this->i18n_strings['advsearch'],
434 'date_criteria' => $this->config['date_criteria'],
435 'flag_criteria' => $this->config['flag_criteria'],
436 'email_criteria' => $this->config['email_criteria']);
437
438 $this->rc->output->command('plugin.show', $ret);
439 }
440
441 public function generate_searchbox()
442 {
443 $search_button = new html_inputfield(array('type' => 'submit', 'name' => 'search', 'class' => 'button mainaction', 'value' => $this->i18n_strings['search']));
444 $reset_button = new html_inputfield(array('type' => 'reset', 'name' => 'reset', 'class' => 'button reset', 'value' => $this->i18n_strings['resetsearch']));
445 $save_button = html::tag('input', array('type' => 'submit', 'name' => 'save_the_search', id=> 'save_the_search', 'class' => 'button save_search', 'value' => $this->i18n_strings['save_the_search']));
446 $delete_button = new html_inputfield(array('type' => 'submit', 'name' => 'delete', 'style' => 'display: none;', 'class' => 'button delete_search', 'value' => $this->i18n_strings['deletesearch']));
447
448 $layout_table = new html_table();
449 $layout_table->add(null, $search_button->show());
450 $folderConfig = array('name' => 'folder');
451 $layout_table->add(
452 null,
453 $this->i18n_strings['in'] . ': ' .
454 $this->folder_selector($folderConfig)->show($this->rc->storage->get_folder()) .
455 html::span(
456 array('class' => 'sub-folders'),
457 $this->i18n_strings['andsubfolders'] . ': ' .
458 html::tag(
459 'input',
460 array('type' => 'checkbox', 'name' => 'subfolder'),
461 null
462 )
463 ) .
464 $this->i19n_strings['where']
465 );
466 $first_row = $this->add_row(true);
467 $layout_table->add_row();
468 $layout_table->add(array('class' => 'adv-search-and-or'), null);
469 $layout_table->add(null, $first_row);
470 $layout_table->add_row();
471 $layout_table->add(null, $search_button->show());
472 $layout_table->add(null, $save_button . ' ' . $reset_button->show() . ' ' . $delete_button->show());
473
474 return html::tag(
475 'div',
476 array('id' => 'adsearch-popup'),
477 html::tag(
478 'form',
479 array('method' => 'post', 'action' => '#'),
480 $layout_table->show()
481 )
482 );
483 }
484
485 /**
486 * This function is used to render the html of the advanced search form and also
487 * the later following rows are created by this function
488 *
489 * @param array $folders Array of folders
490 * @param boolean $first True if form gets created, False to create a new row
491 * @access public
492 * @return string The final html
493 */
494 public function add_row($first = false)
495 {
496 $row_html = '';
497 $optgroups = '';
498
499 $criteria = $this->config['criteria'];
500 $all_criteria = array(
501 $this->gettext('Common') => $this->config['prefered_criteria'],
502 $this->gettext('Addresses') => $this->config['email_criteria'],
503 $this->gettext('Dates') => $this->config['date_criteria'],
504 $this->gettext('Flags') => $this->config['flag_criteria'],
505 $this->gettext('Other') => $this->config['other_criteria'],
506 );
507
508 foreach ($all_criteria as $label => $specific_criteria) {
509 $options = '';
510
511 foreach ($specific_criteria as $value) {
512 $options .= html::tag('option', array('value' => $value), $criteria[$value]);
513 }
514
515 $optgroups .= html::tag('optgroup', array('label' => $label), $options);
516 }
517
518 $tmp = html::tag('select', array('name' => 'filter'), $optgroups);
519 $tmp .= $this->i18n_strings['not'] . ':' . html::tag('input', array('type' => 'checkbox', 'name' => 'not'), null);
520 $tmp .= html::tag('input', array('type' => 'text', 'name' => 'filter-val'));
521 $tmp .= $this->i18n_strings['exclude'] . ':' . html::tag('input', array('type' => 'checkbox', 'name' => 'filter-exclude'), null);
522 $tmp .= html::tag('button', array('name' => 'add', 'class' => 'add'), $this->i18n_strings['addfield']);
523
524 if ($first) {
525 $row_html = $tmp;
526 } else {
527 $and_or_select = new html_select(array('name' => 'method'));
528 $and_or_select->add($this->i18n_strings['and'], 'and');
529 $and_or_select->add($this->i18n_strings['or'], 'or');
530 $tmp .= html::tag('button', array('name' => 'delete', 'class' => 'delete'), $this->i18n_strings['delete']);
531 $row_html = html::tag(
532 'tr',
533 null,
534 html::tag(
535 'td',
536 array('class' => 'adv-search-and-or'),
537 $and_or_select->show()
538 ) .
539 html::tag(
540 'td',
541 null,
542 $tmp
543 )
544 );
545 }
546
547 return $row_html;
548 }
549
550 /**
551 * Return folders list as html_select object
552 *
553 * This is a copy of the core function and adapted to fit
554 * the needs of the advanced_search function
555 *
556 * @param array $p Named parameters
557 *
558 * @return html_select HTML drop-down object
559 */
560 public function folder_selector($p = array())
561 {
562 $p += array('maxlength' => 100, 'realnames' => false, 'is_escaped' => true);
563 $a_mailboxes = array();
564 $storage = $this->rc->get_storage();
565
566 $list = $storage->list_folders_subscribed();
567 $delimiter = $storage->get_hierarchy_delimiter();
568
569 foreach ($list as $folder) {
570 $this->rc->build_folder_tree($a_mailboxes, $folder, $delimiter);
571 }
572
573 $select = new html_select($p);
574 $select->add($this->i18n_strings['allfolders'], "all");
575
576 $this->rc->render_folder_tree_select($a_mailboxes, $mbox, $p['maxlength'], $select, $p['realnames'], 0, $p);
577
578 return $select;
579 }
580
581 public function trigger_search_pagination($param)
582 {
583 $_GET['search'] = $_SESSION['av_search'];
584 $_GET['folder'] = $_SESSION['av_folder'];
585 $_GET['sub_folders'] = $_SESSION['av_sub_folders'];
586 $this->trigger_search(true);
587 }
588
589 /**
590 * Here is where the actual query is fired to the imap server and the result is evaluated and sent back to the client side
591 *
592 * @access public
593 * @return null
594 */
595 public function trigger_search($inPagination = false)
596 {
597 $search = rcube_utils::get_input_value('search', rcube_utils::INPUT_GPC);
598 // reset list_page and old search results
599 $this->rc->storage->set_page(1);
600 $this->rc->storage->set_search_set(null);
601 $page = rcube_utils::get_input_value('_page', rcube_utils::INPUT_GPC);
602 $page = $page ? $page : 1;
603 $pagesize = $this->rc->storage->get_pagesize();
604
605 if (!empty($search)) {
606 $mbox = rcube_utils::get_input_value('folder', rcube_utils::INPUT_GPC);
607 $imap_charset = RCMAIL_CHARSET;
608 $sort_column = rcmail_sort_column();
609 $search_str = $this->get_search_query($search);
610 $sub_folders = rcube_utils::get_input_value('sub_folders', rcube_utils::INPUT_GPC) == 'true';
611 $folders = array();
612 $result_h = array();
613 $count = 0;
614 $new_id = 1;
615 $current_mbox = $this->rc->storage->get_folder();
616 $uid_list = array();
617 //Store information in session for pagination
618 $_SESSION['av_search'] = $search;
619 $_SESSION['av_folder'] = $mbox;
620 $_SESSION['av_sub_folders'] = rcube_utils::get_input_value('sub_folders', rcube_utils::INPUT_GPC);
621 $nosub = $sub_folders;
622 $folders = $this->rc->get_storage()->list_folders_subscribed();
623 if (empty($folders) || ($sub_folders === false && $mbox !== 'all')) {
624 $folders = array($mbox);
625 } elseif ($mbox !== 'all') {
626 if ($sub_folders === false) {
627 $folders = array($mbox);
628 } else {
629 $folders = $this->rc->get_storage()->list_folders_subscribed_direct($mbox);
630 }
631 }
632 $md5folders = array();
633 foreach ($folders as $folder) {
634 $md5folders[md5($folder)] = $folder;
635 }
636 $this->rc->output->set_env('as_md5_folders', $md5folders);
637
638 if ($search_str) {
639 $res = $this->perform_search($search_str, $folders, $page);
640 $count = $res['count'];
641 }
642
643
644 if ($count > 0) {
645 $_SESSION['advanced_search']['uid_list'] = $uid_list;
646 if ($search_str && $inPagination == false) {
647 $this->rc->output->show_message('searchsuccessful', 'confirmation', array('nr' => $count));
648 }
649 } elseif ($err_code = $this->rc->storage->get_error_code()) {
650 rcmail_display_server_error();
651 } else {
652 $this->rc->output->show_message('searchnomatch', 'notice');
653 }
654
655 $current_folder = rcube_utils::get_input_value('current_folder', rcube_utils::INPUT_GPC);
656
657 $this->rc->output->set_env('search_request', 'advanced_search_active');
658 $this->rc->output->set_env('messagecount', $count);
659 $this->rc->output->set_env('pagecount', ceil($count / $pagesize));
660 $this->rc->output->set_env('exists', $this->rc->storage->count($current_folder, 'EXISTS'));
661 $this->rc->output->command('set_rowcount', rcmail_get_messagecount_text($count, $page));
662 $this->rc->output->command('plugin.search_complete');
663 $this->rc->output->send();
664 }
665 }
666
667 /**
668 * return javascript commands to add rows to the message list
669 */
670 public function rcmail_js_message_list($a_headers, $insert_top = false, $a_show_cols = null, $avmbox = false, $avcols = array(), $showMboxColumn = false)
671 {
672 global $CONFIG, $RCMAIL, $OUTPUT;
673 $uid_mboxes = array();
674
675 if (empty($a_show_cols)) {
676 if (!empty($_SESSION['list_attrib']['columns'])) {
677 $a_show_cols = $_SESSION['list_attrib']['columns'];
678 } else {
679 $a_show_cols = is_array($CONFIG['list_cols']) ? $CONFIG['list_cols'] : array('subject');
680 }
681 } else {
682 if (!is_array($a_show_cols)) {
683 $a_show_cols = preg_split('/[\s,;]+/', strip_quotes($a_show_cols));
684 }
685 $head_replace = true;
686 }
687
688 $mbox = $RCMAIL->storage->get_folder();
689
690 // make sure 'threads' and 'subject' columns are present
691 if (!in_array('subject', $a_show_cols)) {
692 array_unshift($a_show_cols, 'subject');
693 }
694 if (!in_array('threads', $a_show_cols)) {
695 array_unshift($a_show_cols, 'threads');
696 }
697 $_SESSION['list_attrib']['columns'] = $a_show_cols;
698
699 // Make sure there are no duplicated columns (#1486999)
700 $a_show_cols = array_merge($a_show_cols, $avcols);
701 $a_show_cols = array_unique($a_show_cols);
702
703 // Plugins may set header's list_cols/list_flags and other rcube_message_header variables
704 // and list columns
705 $plugin = $RCMAIL->plugins->exec_hook(
706 'messages_list',
707 array('messages' => $a_headers, 'cols' => $a_show_cols)
708 );
709 $a_show_cols = $plugin['cols'];
710 $a_headers = $plugin['messages'];
711 $thead = $head_replace ? rcmail_message_list_head($_SESSION['list_attrib'], $a_show_cols) : null;
712
713 // get name of smart From/To column in folder context
714 if (($f = array_search('fromto', $a_show_cols)) !== false) {
715 $smart_col = rcmail_message_list_smart_column_name();
716 }
717 if ($this->coltypesSet == false) {
718 $OUTPUT->command('set_message_coltypes', $a_show_cols, $thead, $smart_col);
719 if ($showMboxColumn === true) {
720 $OUTPUT->command('plugin.advanced_search_add_header', array());
721 }
722 $this->coltypesSet = true;
723 }
724
725 if (empty($a_headers)) {
726 return;
727 }
728
729 // remove 'threads', 'attachment', 'flag', 'status' columns, we don't need them here
730 foreach (array('threads', 'attachment', 'flag', 'status', 'priority') as $col) {
731 if (($key = array_search($col, $a_show_cols)) !== false) {
732 unset($a_show_cols[$key]);
733 }
734 }
735
736 // loop through message headers
737 foreach ($a_headers as $n => $header) {
738 if (empty($header)) {
739 continue;
740 }
741 $a_msg_cols = array();
742 $a_msg_flags = array();
743 // format each col; similar as in rcmail_message_list()
744 foreach ($a_show_cols as $col) {
745 $col_name = $col == 'fromto' ? $smart_col : $col;
746
747 if (in_array($col_name, array('from', 'to', 'cc', 'replyto'))) {
748 $cont = rcmail_address_string($header->$col_name, 3, false, null, $header->charset);
749 } elseif ($col == 'subject') {
750 $cont = trim(rcube_mime::decode_header($header->$col, $header->charset));
751 if (!$cont) {
752 $cont = rcube_label('nosubject');
753 }
754 $cont = rcube::Q($cont);
755 } elseif ($col == 'size') {
756 $cont = $this->rc->show_bytes($header->$col);
757 } elseif ($col == 'date') {
758 $cont = rcube_utils::format_datestr($header->date,$date_format);
759 } else {
760 $cont = rcube::Q($header->$col);
761 }
762 $a_msg_cols[$col] = $cont;
763 }
764
765 $a_msg_flags = array_change_key_case(array_map('intval', (array) $header->flags));
766 if ($header->depth) {
767 $a_msg_flags['depth'] = $header->depth;
768 } elseif ($header->has_children) {
769 $roots[] = $header->uid;
770 }
771 if ($header->parent_uid) {
772 $a_msg_flags['parent_uid'] = $header->parent_uid;
773 }
774 if ($header->has_children) {
775 $a_msg_flags['has_children'] = $header->has_children;
776 }
777 if ($header->unread_children) {
778 $a_msg_flags['unread_children'] = $header->unread_children;
779 }
780 if ($header->others['list-post']) {
781 $a_msg_flags['ml'] = 1;
782 }
783 if ($header->priority) {
784 $a_msg_flags['prio'] = (int) $header->priority;
785 }
786 $a_msg_flags['ctype'] = rcube::Q($header->ctype);
787 $a_msg_flags['mbox'] = $mbox;
788 if (!empty($header->list_flags) && is_array($header->list_flags)) {
789 $a_msg_flags = array_merge($a_msg_flags, $header->list_flags);
790 }
791 if (!empty($header->list_cols) && is_array($header->list_cols)) {
792 $a_msg_cols = array_merge($a_msg_cols, $header->list_cols);
793 }
794 if ($showMboxColumn === true) {
795 $a_msg_flags['avmbox'] = $avmbox;
796 }
797
798 $OUTPUT->command(
799 'add_message_row',
800 $header->uid . '__MB__' . md5($mbox),
801 $a_msg_cols,
802 $a_msg_flags,
803 $insert_top
804 );
805 $id = $header->uid . '__MB__' . md5($mbox);
806 $uid_mboxes[$id] = array('uid' => $header->uid, 'mbox' => $mbox, 'md5mbox' => md5($mbox));
807 }
808
809 if ($RCMAIL->storage->get_threading()) {
810 $OUTPUT->command('init_threads', (array) $roots, $mbox);
811 }
812
813 return $uid_mboxes;
814 }
815
816
817 private function do_pagination($folders, $onPage)
818 {
819 $perPage = $this->rc->storage->get_pagesize();
820 $from = $perPage * $onPage - $perPage + 1;
821 $to = $from + $perPage - 1;
822 $got = 0;
823 $pos = 0;
824 $cbox = "";
825 $boxStart = 0;
826 $boxStop = 0;
827 $fetch = array();
828 foreach ($folders as $box => $num) {
829 $i = $num;
830 if ($box != $cbox) {
831 $boxStart = 0;
832 $boxStop = 0;
833 $cbox = $box;
834 }
835 while ($i--) {
836 $pos++;
837 $boxStart++;
838 if ($pos >= $from && $pos <= $to) {
839 if (!isset($fetch[$box])) {
840 $fetch[$box] = array("from" => $boxStart);
841 }
842 $fetch[$box]['to'] = $boxStart;
843 $got++;
844 }
845 }
846 if ($got >= $perPage) {
847 break;
848 }
849 }
850
851 return $fetch;
852 }
853
854 /**
855 * Save advanced search preferences
856 *
857 * @access public
858 */
859 public function preferences_save($args)
860 {
861 if ($args['section'] != 'advancedsearch') {
862 return;
863 }
864 $rcmail = rcmail::get_instance();
865
866 $displayOptions = array();
867 $displayOptions['_show_message_label_header'] = rcube_utils::get_input_value('_show_message_label_header', rcube_utils::INPUT_POST) == 1 ? true : false;
868 $displayOptions['_show_message_mbox_info'] = rcube_utils::get_input_value('_show_message_mbox_info', rcube_utils::INPUT_POST) == 1 ? true : false;
869 $displayOptions['target_menu'] = rcube_utils::get_input_value('button_display_option', rcube_utils::INPUT_POST);
870
871 $args['prefs']['advanced_search_display_options'] = $displayOptions;
872
873 return($args);
874 }
875
876 /**
877 * Add a section advanced search to the preferences section list
878 *
879 * @access public
880 */
881 public function preferences_section($args)
882 {
883 $args['list']['advancedsearch'] = array(
884 'id' => 'advancedsearch',
885 'section' => rcube::Q($this->gettext('advancedsearch'))
886 );
887
888 return($args);
889 }
890
891 /**
892 * Display advanced search configuration in user preferences tab
893 *
894 * @access public
895 */
896 public function preferences_list($args)
897 {
898 if ($args['section'] == 'advancedsearch') {
899
900 $this->rc = rcmail::get_instance();
901 $args['blocks']['label_display_options'] = array('options' => array(), 'name' => rcube::Q($this->gettext('label_display_options')));
902
903 $displayOptions = $this->rc->config->get('advanced_search_display_options', array());
904 $target_menu = (isset($displayOptions['target_menu']) && !empty($displayOptions['target_menu'])) ? $displayOptions['target_menu'] : $this->config['target_menu'];
905 $options = '';
906 $optarg = array('value' => 'messagemenu');
907 if ($target_menu == 'messagemenu') {
908 $optarg['selected'] = 'selected';
909 $target_image = 'menu_location_a.jpg';
910 }
911 $options .= html::tag('option', $optarg, rcube::Q($this->gettext('display_in_messagemenu')));
912 $optarg = array('value' => 'toolbar');
913 if ($target_menu == 'toolbar') {
914 $optarg['selected'] = 'selected';
915 $target_image = 'menu_location_b.jpg';
916 }
917 $options .= html::tag('option', $optarg,rcube::Q($this->gettext('display_in_toolbar')));
918 $select = html::tag('select', array('name' => 'button_display_option', 'id' => 'button_display_option'), $options);
919
920 $label1 = html::label('_show_message_label_header', rcube::Q($this->gettext('mailbox_headers_in_results')));
921 $label2 = html::label('_show_message_mbox_info', rcube::Q($this->gettext('mailbox_info_in_results')));
922 $label3 = html::label('button_display_option', rcube::Q($this->gettext('show_advanced_search')));
923
924 $arg1 = array('name' => '_show_message_label_header', 'id' => '_show_message_label_header', 'type' => 'checkbox', 'title' => "", 'class' => 'watermark linput', 'value' => 1);
925 if (isset($displayOptions['_show_message_label_header']) && $displayOptions['_show_message_label_header'] === true) {
926 $arg1['checked'] = 'checked';
927 $img1class = 'enabled';
928 } else {
929 $img1class = 'disabled';
930 }
931 $check1 = html::tag('input', $arg1);
932 $arg2 = array('name' => '_show_message_mbox_info', 'id' => '_show_message_mbox_info', 'type' => 'checkbox', 'title' => "", 'class' => 'watermark linput', 'value' => 1);
933 if (isset($displayOptions['_show_message_mbox_info']) && $displayOptions['_show_message_mbox_info'] === true) {
934 $arg2['checked'] = 'checked';
935 $img2class = 'enabled';
936 } else {
937 $img2class = 'disabled';
938 }
939
940 $img1 = html::img(array('src' => $this->url('skins/larry/images/show_mbox_row.jpg'), 'class' => $img1class));
941 $img2 = html::img(array('src' => $this->url('skins/larry/images/show_mbox_col.jpg'), 'class' => $img2class));
942 $img3 = html::img(array('src' => $this->url('skins/larry/images/' . $target_image)));
943
944 $check2 = html::tag('input', $arg2);
945 $args['blocks']['label_display_options']['options'][0] = array('title' => '', 'content' => '<p class="avsearchpref"><span>' . $check1 . ' ' . $label1 . '</span> ' . $img1 . '</p>');
946 $args['blocks']['label_display_options']['options'][1] = array('title' => '', 'content' => '<p class="avsearchpref"><span>' . $check2 . ' ' . $label2 . '</span> ' . $img2 . '</p>');
947 $args['blocks']['label_display_options']['options'][2] = array('title' => '', 'content' => '<p class="avsearchpref"><span>' . $label3 . ' ' . $select . '</span> ' . $img3 . '</p>');
948 }
949
950 return($args);
951 }
952
953 private function perform_search($search_string, $folders, $page = 1)
954 {
955 // Search all folders and build a final set
956 if ($folders[0] == 'all' || empty($folders)) {
957 $folders_search = $this->rc->imap->list_folders_subscribed();
958 } else {
959 $folders_search = $folders;
960 }
961 $count = 0;
962 $folder_count = array();
963 foreach ($folders_search as $mbox) {
964 $this->rc->storage->set_folder($mbox);
965 $this->rc->storage->search($mbox, $search_string, RCMAIL_CHARSET, $_SESSION['sort_col']);
966 $result = array();
967 $fcount = $this->rc->storage->count($mbox, 'ALL', !empty($_REQUEST['_refresh']));
968 $count += $fcount;
969 $folder_count[$mbox] = $fcount;
970 }
971 foreach ($folder_count as $k => $v) {
972 if ($v == 0) {
973 unset($folder_count[$k]);
974 }
975 }
976 $fetch = $this->do_pagination($folder_count, $page);
977 $mails = array();
978 $currentMailbox = "";
979 $displayOptions = $this->rc->config->get('advanced_search_display_options', array());
980 $showMboxColumn = isset($displayOptions['_show_message_mbox_info']) && $displayOptions['_show_message_mbox_info'] ? true : false;
981 $uid_mboxes = array();
982 foreach ($fetch as $mailbox => $data) {
983 if ($currentMailbox != $mailbox) {
984 $currentMailbox = $mailbox;
985 if (isset($displayOptions['_show_message_label_header']) && $displayOptions['_show_message_label_header'] === true) {
986 $this->rc->output->command('advanced_search_add_mbox', $mailbox, $folder_count[$mailbox], $showMboxColumn);
987 }
988 }
989 $uid_mboxes = array_merge($uid_mboxes, $this->getMails($mailbox, $data, $search_string, $showMboxColumn));
990 }
991
992 return array('result' => array(), 'count' => $count, 'uid_mboxes' => $uid_mboxes);
993 }
994
995 private function getMails($mailbox, $data, $search_string, $showMboxColumn)
996 {
997 $pageSize = $this->rc->storage->get_pagesize();
998 $msgNum = $data['from'];
999 $startPage = ceil($msgNum/$pageSize);
1000 $msgMod = $msgNum % $pageSize;
1001 $multiPage = "false";
1002 $firstArrayElement = $msgMod == 0 ? ($pageSize-1) : ($msgMod-1);
1003 $quantity = $data['to'] - $data['from'];
1004 if ($data['from'] + $quantity > $pageSize) {
1005 $multiPage = "true";
1006 }
1007 $this->rc->storage->set_folder($mailbox);
1008 $this->rc->storage->search($mailbox, $search_string, RCMAIL_CHARSET, $_SESSION['sort_col']);
1009 $messages = $this->rc->storage->list_messages('', $startPage);
1010 if ($multiPage) {
1011 $messages = array_merge($messages, $this->rc->storage->list_messages('', $startPage+1));
1012 }
1013 //FIRST: 0 QUANTITY: 2
1014 $sliceTo = $quantity + 1;
1015 $mslice = array_slice($messages, $firstArrayElement, $sliceTo, true);
1016 $messages = $mslice;
1017 $avbox = array();
1018 $showAvmbox = false;
1019 foreach ($messages as $set_flag) {
1020 $set_flag->flags['skip_mbox_check'] = true;
1021 if ($showMboxColumn === true) {
1022 $set_flag->avmbox = $mailbox;
1023 $avbox[] = 'avmbox';
1024 $showAvmbox = true;
1025 }
1026 }
1027 $uid_mboxes = $this->rcmail_js_message_list($messages, false, null, $showAvmbox, $avbox, $showMboxColumn);
1028
1029 return $uid_mboxes;
1030 }
1031
1032 public function save_search()
1033 {
1034 $search_name = rcube_utils::get_input_value('search_name', rcube_utils::INPUT_GPC);
1035 if ($search_name) {
1036 $search = array();
1037 $search['search'] = rcube_utils::get_input_value('search', rcube_utils::INPUT_GPC);
1038 $search['search_name'] = $search_name;
1039 $search['folder'] = rcube_utils::get_input_value('folder', rcube_utils::INPUT_GPC);
1040 $search['sub_folders'] = rcube_utils::get_input_value('sub_folders', rcube_utils::INPUT_GPC);
1041 $prefs = (array)$this->rc->user->get_prefs();
1042 if (!isset($prefs['advanced_search'])) {
1043 $prefs['advanced_search'] = array();
1044 }
1045 $prefs['advanced_search'][$search_name] = $search;
1046 $this->rc->user->save_prefs(array('advanced_search' => $prefs['advanced_search']));
1047 $this->rc->output->show_message('"<i>' . $search_name . '</i>" ' . $this->i18n_strings['has_been_saved'], 'confirmation');
1048 }
1049 }
1050
1051 public function delete_search()
1052 {
1053 $search_name = rcube_utils::get_input_value('search_name', rcube_utils::INPUT_GPC);
1054 if ($search_name) {
1055 $prefs = (array)$this->rc->user->get_prefs();
1056 unset($prefs['advanced_search'][$search_name]);
1057 $this->rc->user->save_prefs(array('advanced_search' => $prefs['advanced_search']));
1058 $this->rc->output->show_message('"<i>' . $search_name . '</i>" ' . $this->i18n_strings['has_been_deleted'], 'notice');
1059 }
1060 }
1061
1062 public function get_saved_search()
1063 {
1064 $search_name = rcube_utils::get_input_value('search_name', rcube_utils::INPUT_GPC);
1065 $prefs = (array)$this->rc->user->get_prefs();
1066 if (!isset($prefs['advanced_search'])) {
1067 $prefs['advanced_search'] = array();
1068 }
1069
1070 $search = isset($prefs['advanced_search'][$search_name]) ? $prefs['advanced_search'][$search_name] : false;
1071 $this->rc->output->command('plugin.load_saved_search', $search);
1072 $this->rc->output->send();
1073 }
1074
1075 private function get_saved_search_names()
1076 {
1077 $prefs = (array)$this->rc->user->get_prefs();
1078 if (!isset($prefs['advanced_search'])) {
1079 $prefs['advanced_search'] = array();
1080 }
1081 $names = array();
1082 foreach($prefs['advanced_search'] as $name => $search) {
1083 $names[] = $name;
1084 }
1085
1086 return $names;
1087 }
1088 }